Virtual currency system for teams. Give kudos, catch lateness, run QA sessions, spend in the shop.
Built by The Mobile First Company. Used daily by our team of 20+.
Kudoz is an internal virtual currency that turns peer recognition into a game. Every day, each team member gets 6 kudoz to give to teammates for great work. Recipients accumulate kudoz in their bank and spend them on real rewards (AirPods, team lunches, a ping pong table).
It ships as a Slack bot + web dashboard, backed by Supabase.
- Every team member gets 6 kudoz per day to distribute
- Give 1-6 kudoz per transaction to one or multiple recipients
- Amounts split evenly across multiple recipients
- Unused daily kudoz do not carry over
- You cannot give kudoz to yourself
- Authorized users can start QA sessions lasting 30 minutes
- During a session, any kudoz with QA-related descriptions (
qa,bug,test,fix,issue) get multiplied by 2x to 5x - One active session at a time
- Report teammates who are late to meetings (1-60 minutes)
- Creates a public "Wall of Shame" on the leaderboard
- Deduplication: same person can't be reported twice within 25 minutes
- Personal rewards: Spotify, AirPods, iPhone, Apple Studio Display (20-1000 kudoz)
- Team rewards: Coffee machine, ping pong table, team lunch, karting (30-200 kudoz)
- Purchases go to "pending" status; an admin fulfills them manually
- Pool kudoz toward expensive team rewards
- Set a goal amount and deadline (max 6 months)
- Contributors are tracked; if the deadline passes unfunded, kudoz are refunded
- Birthday: The bank automatically sends 50 kudoz on your birthday
- Anniversary: The bank sends 500 kudoz per year of tenure (e.g., 2 years = 1000 kudoz)
- Both post a celebration message to the Slack channel
- All balances reset to 0 on December 31st at 23:59
- Use it or lose it — spend before year-end!
- Weekly: Friday 6pm — top givers, top receivers, total kudoz given
- Monthly: 1st of the month at 9am — same stats for the past 30 days
- Sent as DMs to all active users
kudoz/
├── shared/ # Types, constants, validators, Supabase client
│ ├── types.ts
│ ├── constants.ts
│ ├── validators.ts
│ ├── supabase.ts
│ └── index.ts
├── bot/ # Slack bot (Socket Mode)
│ └── src/
│ ├── commands/ # /give, /late, /qa, /bank, /invite-kudoz
│ ├── cron/ # daily-reset, birthday, anniversary, digest, qa-reminder, yearly-reset
│ ├── events/ # team_join auto-registration
│ ├── services/ # kudoz, late, shop (DB operations)
│ ├── middleware/ # rate-limit, input validation
│ └── utils/ # Slack block builders, formatters
├── web/ # Next.js 16 dashboard
│ └── src/
│ ├── app/ # Pages: feed, banks, shop, admin, login
│ ├── components/ # UI components + kudoz-specific cards
│ └── lib/ # Auth, Supabase clients, utilities
├── supabase/ # SQL schema + migrations
├── Dockerfile # Bot container for Railway
└── .env.example # All environment variables
Slack Command → Bot (Socket Mode) → Supabase RPC (send_kudoz) → DB
↓
Web Dashboard ← Supabase Realtime ← Transactions table ←────────┘
| Component | Technology |
|---|---|
| Bot | @slack/bolt 4.x, Socket Mode, node-cron |
| Web | Next.js 16, React 19, Tailwind CSS 4, shadcn/ui |
| Database | Supabase (PostgreSQL + Realtime + Storage + RLS) |
| Auth | Magic link (Resend email + JWT) + admin password |
| Monorepo | pnpm workspaces |
| Bot deploy | Docker on Railway (or any container host) |
| Web deploy | Vercel |
| Table | Purpose |
|---|---|
users |
Team members with Slack ID, balance, roles, birthday, start date, country |
transactions |
Every kudoz movement (give, bank_give, birthday, anniversary, purchase, cagnote) |
daily_allowances |
Tracks remaining daily kudoz per user per day |
late_reports |
Who was late, how many minutes, which meeting |
rewards |
Shop items with price, category, stock, image |
purchases |
Purchase records with status (pending/fulfilled/cancelled) |
cagnotes |
Group purchase pools with goal, deadline, status |
cagnote_contributions |
Individual contributions to cagnotes |
qa_sessions |
Active QA multiplier sessions |
system_settings |
Key-value settings (last reset dates) |
The heart of the system. This PostgreSQL function handles:
- Sender validation
- Self-send prevention (except for bank-originated types)
- Daily allowance check and deduction
- Active QA session detection (keyword matching for multiplier)
- Even split across multiple recipients
- Atomic balance updates and transaction recording
All in a single database transaction — no partial states.
- All tables have RLS enabled
- Public
SELECTon all tables (the dashboard is transparent by design) - Writes only via
service_rolekey (bot and API routes)
Give kudoz to teammates. Amount is split across multiple recipients.
/give 6 @alice Great work on the API refactor
/give 3 @bob @charlie Excellent pair programming session
Report lateness to a meeting.
/late 15 @bob standup
/late 5 @alice @charlie sprint review
Start a QA session with a multiplier (authorized users only).
/qa start 3 Testing the new checkout flow
Check your balance, daily allowance remaining, and last 5 transactions.
Add a new team member to the system (all fields optional except the @mention).
| Route | Description |
|---|---|
/ |
Live feed with realtime transaction stream, contribution heatmap, search & filters |
/banks |
Leaderboards (best givers, most received, wall of shame), all bank accounts |
/banks/[id] |
Individual bank detail — balance, received, given, late stats, transaction history |
/shop |
Rewards catalog, cagnote progress bars, purchase flow |
/login |
Magic link login (email) |
/admin |
Bank give, reward management, purchase fulfillment, user management, team events calendar |
| Schedule | Job | Description |
|---|---|---|
| Midnight daily | daily-reset |
Cleans up allowance rows older than 7 days |
| 9am daily | birthday |
Checks for birthdays, awards 50 kudoz, posts to channel |
| 9am daily | anniversary |
Checks for work anniversaries, awards 500*years kudoz |
| Friday 6pm | digest (weekly) |
DMs weekly stats to all users |
| 1st of month 9am | digest (monthly) |
DMs monthly stats to all users |
| Thursday 6pm | qa-reminder |
Reminds QA starters to plan Friday sessions |
| Dec 31 23:59 | yearly-reset |
Resets all balances to 0 |
All times in Europe/Paris timezone (configurable in shared/constants.ts).
Go to supabase.com, create a new project. Note your:
- Project URL (
https://xxxxx.supabase.co) - Anon key (public)
- Service role key (secret)
In the Supabase SQL Editor, run these files in order:
supabase/001_initial.sql— Creates all tables, RPC functions, RLS policiessupabase/002_add_user_country.sql— Adds country column
Then run supabase/seed.sql to insert example data (edit with your real team members first).
- Go to api.slack.com/apps
- Click "Create New App" → "From a manifest"
- Paste the contents of
slack-app-manifest.yaml - Install to your workspace
- Note your:
- Bot Token (
xoxb-...) from OAuth & Permissions - App Token (
xapp-...) from Basic Information → App-Level Tokens (create one withconnections:writescope) - Signing Secret from Basic Information
- Bot Token (
- Create a
#kudozchannel and note its Channel ID
Copy .env.example to .env.local and fill in all values:
cp .env.example .env.local| Variable | Where to find it |
|---|---|
SLACK_BOT_TOKEN |
Slack app → OAuth & Permissions |
SLACK_APP_TOKEN |
Slack app → Basic Information → App-Level Tokens |
SLACK_SIGNING_SECRET |
Slack app → Basic Information |
KUDOZ_CHANNEL_ID |
Right-click channel → Copy Channel ID |
NEXT_PUBLIC_SUPABASE_URL |
Supabase → Settings → API |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Supabase → Settings → API |
SUPABASE_SERVICE_ROLE_KEY |
Supabase → Settings → API |
ADMIN_PASSWORD |
Choose a strong password |
ADMIN_SECRET |
Random string (for admin cookie) |
AUTH_SECRET |
Random string (for JWT signing) |
RESEND_API_KEY |
resend.com → API Keys |
NOTIFICATION_EMAIL |
Email for purchase notifications |
The bot runs as a Docker container. Deploy to Railway, Fly.io, or any Docker host.
Railway:
# Install Railway CLI
npm i -g @railway/cli
# Login and init
railway login
railway init
# Set env vars
railway variables set SLACK_BOT_TOKEN=xoxb-...
railway variables set SLACK_APP_TOKEN=xapp-...
# ... set all variables from .env.example
# Deploy
railway upOr Docker directly:
docker build -t kudoz-bot .
docker run --env-file .env.local kudoz-botVercel (recommended):
# Install Vercel CLI
npm i -g vercel
# Deploy from the repo root
cd web
vercel --prodSet the same environment variables in Vercel's dashboard (Settings → Environment Variables).
Root directory: web/
Build command: pnpm build
Framework: Next.js
Edit supabase/seed.sql with your real team members' data, then:
- Replace
SLACK_ID_*placeholders with actual Slack user IDs - Set real names, emails, birthdays, start dates
- Run in the Supabase SQL Editor
Or use the /invite-kudoz Slack command to add users one by one.
| Variable | Required | Used By | Description |
|---|---|---|---|
SLACK_BOT_TOKEN |
Bot | Bot | Slack bot OAuth token |
SLACK_APP_TOKEN |
Bot | Bot | Slack app-level token for Socket Mode |
SLACK_SIGNING_SECRET |
Bot | Bot | Slack request signing secret |
KUDOZ_CHANNEL_ID |
Bot | Bot | Channel ID for public announcements |
NEXT_PUBLIC_SUPABASE_URL |
Both | Web + Bot | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Web | Web | Supabase anonymous key |
SUPABASE_SERVICE_ROLE_KEY |
Both | Web + Bot | Supabase service role key |
SUPABASE_URL |
Bot | Bot | Supabase URL (alias) |
DATABASE_URL |
Optional | Migrations | Direct PostgreSQL connection |
ADMIN_PASSWORD |
Web | Web | Admin panel password |
ADMIN_SECRET |
Web | Web | Admin cookie signing secret |
AUTH_SECRET |
Web | Web | JWT signing secret for magic links |
RESEND_API_KEY |
Web | Web | Resend API key for emails |
NOTIFICATION_EMAIL |
Web | Web | Email for purchase notifications |
All tunable values live in shared/constants.ts:
| Constant | Default | Description |
|---|---|---|
DAILY_ALLOWANCE |
6 | Kudoz each person can give per day |
MAX_PER_GIVE |
6 | Max kudoz in a single /give command |
MIN_PER_GIVE |
1 | Min kudoz in a single /give command |
MAX_LATE_MINUTES |
60 | Max minutes for a late report |
LATE_DEDUP_WINDOW_MINUTES |
25 | Prevent duplicate reports within this window |
QA_MIN_MULTIPLIER |
2 | Minimum QA session multiplier |
QA_MAX_MULTIPLIER |
5 | Maximum QA session multiplier |
QA_DURATION_MINUTES |
30 | How long a QA session lasts |
BIRTHDAY_BONUS |
50 | Kudoz awarded on birthday |
ANNIVERSARY_BASE_BONUS |
500 | Kudoz per year of tenure |
CAGNOTE_DEADLINE_MONTHS |
6 | Max deadline for group purchases |
TIMEZONE |
Europe/Paris | Timezone for all cron jobs |
RATE_LIMIT_WINDOW_MS |
60000 | Rate limit window (1 min) |
RATE_LIMIT_MAX_COMMANDS |
20 | Max commands per window |
COUNTRIES |
AR, FR, US | Office country options |
ADMINS |
[] | Slack usernames with admin rights |
QA_STARTERS |
[] | Slack usernames who can start QA |
Edit DAILY_ALLOWANCE in shared/constants.ts. Also update the hardcoded 6 in the send_kudoz() RPC function in supabase/001_initial.sql.
Use the admin panel (/admin → Rewards tab) or insert directly into the rewards table.
Edit TIMEZONE in shared/constants.ts. All cron jobs use this value.
Search for @yourcompany.com in the codebase and replace with your domain:
web/src/app/login/page.tsxweb/src/app/api/auth/login/route.ts
Update the from field in:
web/src/app/api/auth/login/route.tsweb/src/app/api/shop/purchase/route.ts
Edit the COUNTRIES array in shared/constants.ts.
Edit ADMINS and QA_STARTERS in shared/constants.ts with your Slack usernames (lowercase).
# Install dependencies
pnpm install
# Copy env vars
cp .env.example .env.local
# Fill in all values
# Start the web dashboard (port 4005)
pnpm dev:web
# Start the Slack bot (connects via Socket Mode)
pnpm dev:botSee CONTRIBUTING.md — oh wait, we removed it. Here's the gist:
- Fork this repo
- Create a branch
- Make your changes
- Open a PR
MIT — see LICENSE.
Built with love at The Mobile First Company.