Internal Slack recognition program for the wlt-and-shaman workspace. Inspired by HeyTaco.
Give ๐ฎ reactions to teammates in #taqueria. Every employee has a daily allowance (default 5). Tacos accumulate as a balance that can be redeemed in the shop โ HR-mediated.
- Give by reaction: react to a teammate's message with ๐ฎ โ they get 1 taco.
- Give by mention: post
<@teammate> :taco: :taco:in an allowlisted channel โ 2 tacos to that teammate. Multi-recipient gives split the count per recipient. - Daily allowance: each user gets
TACO_DAILY_ALLOWANCEtacos to give per day, auto-reset at 00:00 UTC by a Vercel cron. - Two counters per user: lifetime
received_total(for the leaderboard) and currentbalance(redeemable). - DM commands:
score,balance,left,shop,helpโ English and French aliases. - Public shop:
/shoplists active items with prices and descriptions. WhenSHOP_SELF_SERVE_ENABLED=true, signed-in users canBuydirectly โ the purchase atomically debits balance + decrements stock + creates a pending order, and the bot posts an announcement to#tacoshopwithMark fulfilled/Cancel & refundbuttons that admins click without leaving Slack. Buyers see their own pending orders on/shopand can self-cancel. - Admin console:
/admin/items(catalog CRUD with image upload),/admin/users(redemption form),/admin/activity(chronological give feed with reversal status, channel filter, and Slack deep-links), and/admin/leaderboard(ranked list with metric/period/channel filters). All gated by Sign in with Slack againstADMIN_SLACK_IDS. - Append-only audit log: every give, redemption, and reversal is a row in
transactionswith the channel, message timestamp, admin, item, and reason. - Reversible gives: deleting your
:taco:message or removing your ๐ฎ reaction writes a compensatingtype='reversal'row, decrements the recipient's balance, restores your daily allowance (capped at the daily cap), and DMs both parties. - Slack-retry idempotent: each individual taco has a unique
slack_event_id; retries are no-ops, not duplicates. Reversals additionally key onreversed_transaction_idso a single give can be reversed at most once. - Concurrent-safe: gives and redemptions use atomic
UPDATE โฆ WHERE balance/daily_remaining >= Nso concurrent attempts can't overdraw.
- Employees ("how do I give and spend tacos?") โ docs/user-guide.md
- HR / shop admins ("how do I manage items and process redemptions?") โ docs/hr-guide.md
- Engineers ("how is this built? how do I extend it?") โ CLAUDE.md and docs/architecture.md
- Operators ("how do I deploy, run, monitor?") โ continue reading, then docs/operations.md
Next.js 15 + TypeScript + Tailwind on Vercel Pro ยท Bolt for JS (Slack Events API) ยท Vercel Postgres + Drizzle ORM ยท Auth.js v5 + Slack OIDC for the admin pages ยท Vitest with pglite for in-process integration tests.
- Vercel Pro (60s function timeout, unlimited cron jobs).
- Slack workspace admin to create the app.
- Vercel Postgres / Neon database attached to the project.
See docs/slack-setup.md for the full checklist (scopes, event subscriptions, OAuth redirect, OIDC scopes).
Copy .env.example to .env.local (for dev) or set them in Vercel project settings.
| Variable | Purpose |
|---|---|
SLACK_BOT_TOKEN |
xoxb-โฆ from app install |
SLACK_SIGNING_SECRET |
App's signing secret |
SLACK_BOT_USER_ID |
Optional; cached from auth.test if absent |
SLACK_CLIENT_ID / SLACK_CLIENT_SECRET |
For Sign in with Slack (admin) |
TACO_CHANNELS |
Comma-separated channel IDs where typed/reaction gives count |
TACO_DAILY_ALLOWANCE |
Defaults to 5 |
TACO_ALT_EMOJI_NAME |
Optional; custom emoji name (no colons) accepted as currency alongside :taco:. When set, the bot's confirmation reaction (if enabled) uses this emoji instead of :taco:. |
TACO_REACT_ON_GIVE |
Optional; if true, the bot adds a :taco: (or alt-emoji) reaction to the giver's message as a visual ack. Defaults to false โ recommended off, since the bot's reaction is easily mistaken for an extra give. |
ADMIN_SLACK_IDS |
Comma-separated Slack user IDs allowed into /admin |
HR_SLACK_ID |
Legacy /shop HR contact link (retires alongside self-serve flag) |
HR_SLACK_HANDLE |
Legacy display handle (without @) for HR contact |
SHOP_SELF_SERVE_ENABLED |
When true, /shop shows Buy buttons + a pending-orders panel and posts to #tacoshop. Defaults to false. |
TACOSHOP_CHANNEL_ID |
Required when self-serve is on: Slack channel ID where order announcements + admin Fulfill / Cancel buttons live |
AUTH_SECRET |
openssl rand -base64 32 |
AUTH_URL |
Auto-set on Vercel via VERCEL_URL |
POSTGRES_URL |
Provided by the Vercel Postgres integration |
NEXT_PUBLIC_SHOP_URL |
Public URL of /shop |
NEXT_PUBLIC_COMPANY_NAME |
Appears in the page <title>; defaults to "WLT" |
CRON_SECRET |
Auto-injected by Vercel for cron requests |
pnpm db:migrate(Vercel runs this automatically as part of pnpm build.)
After deployment, run once to import existing workspace members:
pnpm sync-usersSubsequent joiners are picked up by the team_join event automatically.
In Slack, run /invite @tacobot in #taqueria-beta (or whichever channel(s) you set in TACO_CHANNELS).
The dev container (or any local machine) needs Node 20 and pnpm. The first install requires the project's .npmrc setting (store-dir=/home/node/.pnpm-store) to avoid a copyfile race with pnpm's default project-local store on bind-mounted filesystems.
pnpm installpnpm devThe dev server starts fine without secrets, but the moment a route reads config.slack.botToken (or any required env var) it throws. Provide the values via .env.local before exercising the Slack webhook or admin pages.
Integration tests run against an in-process PGlite โ no Docker, no real Postgres needed locally:
pnpm test
pnpm test:watch # watch modeFor the full development workflow (devcontainer, debugging, testing patterns, migration workflow, CI), see docs/development.md.
Slack must reach a public URL to deliver events. For local dev:
ngrok http 3000(orcloudflared tunnel).- Update the Slack app's Event Subscriptions URL to the ngrok URL with path
/api/slack/events. - Don't forget to set the Slack app's OAuth redirect URL too if testing admin sign-in locally.
For most development, deploying to a Vercel preview branch is simpler than running locally โ Vercel preview deploys are free and reachable from Slack.
- Link the project:
vercel linkfrom the repo, or use the Vercel dashboard "Import Git Repository" flow. - In Vercel โ Storage, create a Postgres / Neon database. This auto-injects
POSTGRES_URLand friends. - Set the env vars from the table above in the Vercel project settings.
- The
vercel.jsonalready declares the daily-reset cron at0 0 * * *(UTC midnight). It appears in Settings โ Cron Jobs after first deploy. - Push to the deploy branch; Vercel runs
pnpm build(which includespnpm db:migrate) andnext build. - After the first deploy, run
pnpm sync-usersonce locally with the productionPOSTGRES_URLset in.env.local, to bulk-import existing workspace members.
| Action | How |
|---|---|
| Add or remove an admin | Update ADMIN_SLACK_IDS in Vercel env, redeploy |
| Change the channel allowlist | Update TACO_CHANNELS, redeploy, /invite @tacobot in any new channel |
| Change the daily allowance | Update TACO_DAILY_ALLOWANCE; the next 00:00 UTC reset refills everyone to the new value |
| Change the daily-reset timezone | Edit the cron expression in vercel.json (UTC; env-vars don't interpolate). Default 0 0 * * * is UTC midnight; e.g. 0 8 * * * = 08:00 UTC |
| Rotate Slack signing secret | Regenerate in Slack dashboard โ update SLACK_SIGNING_SECRET in Vercel โ redeploy |
| Inspect data | pnpm db:studio opens Drizzle Studio against the configured database |
For runbook-level detail (smoke checklist, audit-query cookbook, monitoring, failure-mode cheatsheet, manual balance correction policy), see docs/operations.md.
End-to-end: Slack POSTs to /api/slack/events โ custom AppRouterReceiver verifies the HMAC and short-circuits URL-verification handshakes โ Bolt App dispatches to the message / reaction / command / user-sync handlers in lib/slack/handlers/ โ pure validate/decide logic in lib/slack/give.ts โ atomic transactional execute in lib/slack/execute.ts โ Drizzle โ Postgres. Auth.js v5 with the Slack OIDC provider gates /admin/* (the allowlist check is in the signIn callback, so non-admins never get a session). A daily Vercel cron resets the allowance.
For the full system diagram, data-model rationale, give/redeem flow traces, idempotency and concurrency model, see docs/architecture.md.
Internal use, not published.