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
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
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.
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.
- 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 channelG…, DMD…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.
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 D1npx 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- api.slack.com/apps → Create New App → From a manifest.
- Paste
manifest.json, replacing everyRATIFY_BASE_URLwith the deployed URL. - Copy Client ID / Client Secret / Signing Secret into the Worker secrets above.
- Install: visit
https://<worker-url>/slack/install.
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…'"- 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)