Skip to content

Repository files navigation

Ratify — the decision log for Slack

CI

Teams make decisions in Slack threads and lose them forever. Three months later nobody remembers why you chose Postgres, or who agreed to ship on Fridays. Ratify captures decisions where they happen — with the what, the who, the why and tags — and makes them searchable forever.

  • /decision — full logging form · one-line quick log
  • Right-click any message → Log as decision
  • /decisions [term] — search the log in-channel
  • App Home — the whole register, with author-only delete
  • Monday digest — the week's decisions, posted where the team reads
  • Free: 50 decisions per workspace · Pro: unlimited, $6/workspace/mo

Architecture

Runs entirely on Cloudflare's free tier — no servers, no cold-start pain, a global HTTPS endpoint, and cron built in. ~100k requests/day free covers thousands of workspaces for this workload.

flowchart LR
    Slack -->|"signed webhooks<br/>(commands, events, interactivity)"| W[Cloudflare Worker]
    W --> D1[(D1 / SQLite)]
    W -->|chat.postMessage, views.*| SlackAPI[Slack Web API]
    Cron[Cron: Mon 09:00 UTC] --> W
Loading

Every inbound request is HMAC-verified (Slack request signing, 5-minute replay window, constant-time compare). Bot tokens are AES-256-GCM-encrypted before they touch D1 — a database dump alone leaks nothing. OAuth state is HMAC-signed and expiring, so the install flow is CSRF-safe without a session store.

The scribe: opt-in AI suggestions

Invite @Ratify to a public channel and it watches for decision-shaped messages: a keyword pre-filter gates a Workers AI classifier (the model runs in the same Cloudflare account — conversation text never reaches a third-party AI provider). Hits produce an ephemeral prompt to the message author only — 👥 log for team / 🔒 log only-me / ✏️ edit / dismiss. Nothing is stored without a human click; dismissed suggestions are deleted and unresolved ones purged after 7 days. Logged suggestions keep a permalink to the source message, so every decision links back to the conversation it came from.

Privacy model

  • Reads only what it's invited to. No DM or private-channel read permissions exist in the app at all. Public-channel messages are received only where the bot was explicitly invited — and manual capture (/decision, message shortcut) works everywhere without any reading.
  • Explicit, safe-default visibility. The logging form has an author-chosen visibility: 👥 team-visible or 🔒 only-me. The default derives from the capture context (public channel C… → team-visible; private channel G…, DM D… or unknown → only-me) and fails closed. Only-me decisions never surface for anyone else and are never in the digest. One-line quick logs use the context default.

Local development

npm install
npm run typecheck && npm test          # 31 unit tests, no services needed
cp .dev.vars.example .dev.vars         # fill in test values
npx wrangler d1 execute ratify --local --file=schema.sql
npm run dev                            # http://localhost:8787 with a local D1

Deploy runbook (Cloudflare, free plan)

npx wrangler login                                  # one-time, browser
npx wrangler d1 create ratify                       # paste database_id into wrangler.toml
npx wrangler deploy                                 # note the *.workers.dev URL
#   → set APP_BASE_URL in wrangler.toml to that URL, deploy again
npx wrangler d1 execute ratify --remote --file=schema.sql
npx wrangler secret put SLACK_CLIENT_ID             # next section
npx wrangler secret put SLACK_CLIENT_SECRET
npx wrangler secret put SLACK_SIGNING_SECRET
openssl rand -base64 32 | npx wrangler secret put TOKEN_KEY

Create the Slack app

  1. api.slack.com/appsCreate New App → From a manifest.
  2. Paste manifest.json, replacing every RATIFY_BASE_URL with the deployed URL.
  3. Copy Client ID / Client Secret / Signing Secret into the Worker secrets above.
  4. Install: visit https://<worker-url>/slack/install.

Selling it: Slack Marketplace checklist

What Slack's review requires (all prepared in this repo):

  • OAuth v2 install flow (multi-workspace) — /slack/install
  • Request signature verification on every endpoint
  • Minimal scopes with justification (commands, chat:write, chat:write.public)
  • Privacy policy (/privacy) and terms (/terms) served over HTTPS
  • Support contact (landing page footer)
  • App name, description, long description (manifest)
  • App icon 512×512 (design before submission)
  • Verify the "Ratify" name is available in the directory; rename if taken
  • Demo video / screenshots for the listing
  • Submit via app settings → Submit to Marketplace (review takes ~2–6 weeks)

Billing note: Slack does not process payments — monetization runs outside (this app uses a Stripe Payment Link; set the STRIPE_PAYMENT_LINK secret). Until the Stripe webhook lands (roadmap), flip a paying workspace manually:

npx wrangler d1 execute ratify --remote \
  --command "UPDATE workspaces SET plan='pro' WHERE team_id='T…'"

Roadmap

  • Stripe webhook → automatic plan activation
  • CSV / Notion export (Pro)
  • Decision statuses (proposed → ratified → superseded) with links between them
  • Microsoft Teams port (same core, Bot Framework front end)

License

MIT

About

Ratify — the decision log for Slack. Capture decisions with their why, search them forever. Cloudflare Workers + D1, marketplace-ready.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages