Telegram bot: tag the right people, not everyone.
Members opt into named huddles (devs, design, night-owls). Anyone can ping a
huddle; only the people who joined get notified — and any member can mute a single
huddle without leaving the group. Per-huddle cooldowns stop tag-spam.
- Node 20+ / TypeScript
- grammY — Telegram bot framework
- Postgres (Supabase, transaction pooler) + Drizzle ORM
npm install
copy .env.example .env
Fill in .env:
BOT_TOKEN— from @BotFatherDATABASE_URL— Supabase → Connect → Transaction pooler (port 6543). The direct connection is IPv6-only on the free tier and will not resolve on most networks.
Push the schema, then run:
npm run db:push
npm run dev
Two things to set once, or features won't work:
/setprivacy→ Disable — otherwise the bot can't see regular messages and can't keep display names fresh/setjoingroups→ Enable — on by default, worth confirming
| Command | Who | What |
|---|---|---|
@devs |
anyone | Ping a huddle — the main way to use it |
/create devs |
anyone* | Make a huddle, creator auto-joins |
/join devs |
anyone | Opt in |
/leave devs |
anyone | Opt out |
/huddle devs deploy broke |
anyone | Same as @devs, explicit form |
/mute devs 4h |
anyone | Stay a member, go quiet (30m / 4h / 3d) |
/unmute devs |
anyone | Turn it back on |
/huddles |
anyone | List huddles, ✅ marks yours |
/members devs |
anyone | Who's in it, 🔔/🔕 per member |
/settings |
admin | Cooldown + who can create |
/delete devs |
admin | Remove a huddle |
* unless an admin flips "admins only create" in /settings
api/
telegram.ts Vercel serverless webhook endpoint
scripts/
set-webhook.ts one-off: point Telegram at the deployment
webhook-info.ts ask Telegram what the webhook currently is
src/
bot.ts builds the bot — no side effects, shared by both entries
dev.ts local entrypoint (long polling)
db/
schema.ts chats, huddles, memberships, ping_log + relations
index.ts drizzle client
handlers/
migration.ts group -> supergroup chat_id migration
huddles.ts /create /delete /huddles
membership.ts /join /leave /mute /unmute /members
ping.ts @devs and /huddle — throttle, mute filter, batched mentions
settings.ts /settings admin panel
lib/
context.ts chat upsert middleware, admin check, group guard
text.ts slug + duration parsing, HTML escaping
Local dev runs on long polling (npm run dev). Production runs as a serverless
function — Telegram POSTs each update to api/telegram.ts.
1. Push to GitHub — .env is gitignored; confirm with git status first.
2. Import the repo at vercel.com/new → pick huddle-bot → Deploy.
No build settings to change; the api/ directory is detected automatically.
3. Set environment variables (Project → Settings → Environment Variables). Add each to all three environments:
| Variable | Value |
|---|---|
BOT_TOKEN |
same as local |
DATABASE_URL |
Supabase transaction pooler string (port 6543) |
WEBHOOK_SECRET |
node -e "console.log(require('crypto').randomBytes(24).toString('hex'))" |
Redeploy after adding them — Vercel doesn't apply new variables to an existing deployment.
4. Point Telegram at it. Serverless has no startup hook, so this is a one-off.
Put your production domain and the same secret in your local .env:
WEBHOOK_SECRET=<same value as in Vercel>
WEBHOOK_URL=https://huddle-bot.vercel.app
Then:
npm run webhook:set
5. Verify
npm run webhook:info
URL should show your function and Last error: none. Send @devs test in a
group; check Vercel → Deployments → Functions for the invocation log.
npm run dev deletes the webhook on startup, because polling and webhooks are
mutually exclusive and a stale webhook would silently eat every update. So local
dev takes the bot away from production. When you're done developing, run
npm run webhook:set to hand it back.
npm run webhook:info tells you which mode you're in — that's the first thing to
check whenever the deployed bot goes quiet.
- Cold starts add roughly a second to the first ping after an idle spell.
- Function timeout is 10s on Hobby (
vercel.jsonrequests 30s, which applies on Pro). Pings send one message per 5 recipients, so a huddle in the hundreds could clip. Fine at normal group sizes. - Use the transaction pooler, not the direct connection. Every invocation
opens its own connection; the direct one runs out fast.
prepare: falseinsrc/db/index.tsis required for pgbouncer transaction mode — don't remove it.
- Telegram only fires notifications for the first ~5 mentions in a message, so
ping.tsbatches recipients across multiple messages. - Members without a
@usernameare mentioned viatg://user?id=deep links. - Polling in dev (
src/dev.ts), webhooks in prod (api/telegram.ts).src/bot.tsbuilds the bot and does nothing else, so both entry points share one definition. - Telegram assigns a new chat_id when a group upgrades to a supergroup;
migration.tscatches this and re-points existing huddles. Without it, a group silently loses every huddle the moment it upgrades.
- Scheduled/recurring pings (
/huddle standupevery weekday 9am) - Auto-prune inactive members from huddles
- Smart huddle suggestion from message context
- Per-huddle analytics — response rates, most-pinged