Slack emoji reaction leaderboard. Shows which emojis get used most and who reacts the most across your workspace.
Built as a Bun workspaces monorepo:
| Package | Description | Deployed to |
|---|---|---|
packages/worker |
Catalyst bot — Hono API + Slack event handler (CF Worker) | catalyst-api.scstem.workers.dev |
packages/web |
React frontend (CF Pages / static) | catalyst.scstem.tech |
Stack: Cloudflare Workers + D1, Hono, slack-cloudflare-workers, React 19, TailwindCSS v4
- Bun (v1.3+)
- A Cloudflare account
- A Slack workspace you can create apps in
bun installThis resolves both workspace packages and all their dependencies.
- Go to api.slack.com/apps and click Create New App > From scratch
- Name it whatever you want and pick your workspace
Under OAuth & Permissions, add these Bot Token Scopes:
| Scope | Used for |
|---|---|
reactions:read |
Receiving reaction events |
chat:write |
Bot messages (backfill status, slash command responses) |
commands |
Slash commands (/catalyst, /backfill) |
channels:history |
Backfill: reading message history |
groups:history |
Backfill: private channel history |
channels:read |
Backfill: listing channels |
groups:read |
Backfill: listing private channels |
users:read |
Fetching user profiles (display names + avatars) |
emoji:read |
Fetching custom emoji images |
Under Event Subscriptions:
- Toggle Enable Events on
- Set the Request URL to
https://catalyst.scstem.tech/api/slack/events(come back to this after deploying in step 5) - Under Subscribe to bot events, add:
reaction_addedreaction_removed
Under Slash Commands, click Create New Command:
| Field | Value |
|---|---|
| Command | /catalyst |
| Request URL | https://catalyst.scstem.tech/api/slack/events |
| Short Description | Ping the Catalyst bot |
Then create a second command:
| Field | Value |
|---|---|
| Command | /backfill |
| Request URL | https://catalyst.scstem.tech/api/slack/events |
| Short Description | Backfill emoji reactions for this channel |
The slash commands and events share the same endpoint — the SDK dispatches by payload type.
Under Install App, click Install to Workspace and authorize. Copy the Bot User OAuth Token (xoxb-...).
Under Basic Information > App Credentials, copy the Signing Secret.
cd packages/worker
bunx wrangler d1 create catalyst-dbThis prints a database ID. Paste it into packages/worker/wrangler.jsonc:
Locally (for development):
cd packages/worker
bun run db:migrate:localRemotely (for production):
cd packages/worker
bun run db:migrate:remoteCreate packages/worker/.dev.vars:
SLACK_SIGNING_SECRET=your-signing-secret
SLACK_BOT_TOKEN=xoxb-your-bot-token
cd packages/worker
bunx wrangler secret put SLACK_SIGNING_SECRET
bunx wrangler secret put SLACK_BOT_TOKEN
bunx wrangler secret put SITE_PASSWORD
bunx wrangler secret put TURNSTILE_SECRET_KEY
SITE_PASSWORDandTURNSTILE_SECRET_KEYhave test defaults inwrangler.jsoncfor local development. You must override them with real values in production viawrangler secret put.
cd packages/worker
bun run deployBuild static assets and deploy to Cloudflare Pages (or your preferred host):
cd packages/web
bun run buildAfter deploying the worker, go back to your Slack app's Event Subscriptions and set the Request URL to:
https://catalyst-api.scstem.workers.dev/slack/events
Slack will send a verification challenge — the slack-cloudflare-workers SDK handles this automatically.
The backfill script imports all existing reactions from your Slack workspace into D1. It supports both local and remote (production) targets.
Writes directly to the local D1 SQLite file via bun:sqlite:
export SLACK_BOT_TOKEN=xoxb-your-bot-token
export BACKFILL_SINCE=2025-01-01 # optional: ISO date cutoff
bun scripts/backfill.tsWrites to your production D1 database via the Cloudflare REST API:
export SLACK_BOT_TOKEN=xoxb-your-bot-token
export BACKFILL_TARGET=remote
export CLOUDFLARE_ACCOUNT_ID=your-account-id
export CLOUDFLARE_DATABASE_ID=your-d1-database-id
export CLOUDFLARE_D1_TOKEN=your-api-token
# Optional: ISO date cutoff
export BACKFILL_SINCE=2025-01-01
bun scripts/backfill.tsThe CLOUDFLARE_D1_TOKEN needs the D1 Write permission. You can create one at dash.cloudflare.com/profile/api-tokens.
- List all channels the bot has access to
- Fetch message history (with rate limiting)
- Extract reactions and insert them into D1
- Rebuild aggregate count tables
- Fetch user profiles (display names + avatars)
- Fetch custom emoji image URLs
Note: The bot must be a member of channels you want to backfill. Invite it with
/invite @YourBotNameor add it to channels in the Slack app settings.
Start both services in separate terminals:
# Terminal 1 — Worker (Hono API + Slack handler on :8787)
cd packages/worker
bun run dev
# Terminal 2 — Frontend (Vite on :5173, proxies /api → :8787)
cd packages/web
bun run devVisit http://localhost:5173.
| Script | Description |
|---|---|
bun run typecheck |
TypeScript type checking across all packages (tsc -b) |
bun run check |
Biome lint + format (auto-fix) |
bun run format |
Biome format only (auto-fix) |
bun run lint |
Biome lint only (auto-fix) |
| Script | Description |
|---|---|
bun run dev |
Start wrangler dev server |
bun run deploy |
Deploy to Cloudflare Workers |
bun run db:generate |
Generate Drizzle migration |
bun run db:migrate:local |
Apply D1 schema locally |
bun run db:migrate:remote |
Apply D1 schema to production |
bun run cf-typegen |
Regenerate Cloudflare bindings types |
| Script | Description |
|---|---|
bun run dev |
Start Vite dev server |
bun run build |
Production build |
bun run preview |
Preview production build |
All API routes are defined in packages/worker/src/app.ts — see that file for the authoritative list. The /slack/events endpoint is handled separately in packages/worker/src/index.ts.
┌──────────────────┐ POST /slack/events ┌─────────────────────┐
│ Slack Events │ ──────────────────────────► │ slack-cloudflare- │
│ API │ │ workers SDK │
└──────────────────┘ │ (sig verification, │
│ challenge, parse) │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ addReaction() / │
│ removeReaction() │
│ → D1 batch write │
└─────────────────────┘
┌──────────────────┐ GET /api/* ┌─────────────────────┐
│ React frontend │ ──────────────────────────► │ Hono app (CORS) │
│ (hono/client) │ ◄────────────────────────── │ → Drizzle → D1 │
└──────────────────┘ JSON (type-safe RPC) └─────────────────────┘
The worker entry point (packages/worker/src/index.ts) routes by path:
/slack/events→slack-cloudflare-workersSDK- Everything else → Hono app
Hono RPC type safety is maintained across packages via TypeScript project references — the worker tsconfig emits declarations, and the web tsconfig references it.
{ "d1_databases": [ { "binding": "DB", "database_name": "catalyst-db", "database_id": "<paste-your-id-here>" } ] }