Skip to content

Repository files navigation

Huddle

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.

Stack

  • Node 20+ / TypeScript
  • grammY — Telegram bot framework
  • Postgres (Supabase, transaction pooler) + Drizzle ORM

Setup

npm install
copy .env.example .env

Fill in .env:

  • BOT_TOKEN — from @BotFather
  • DATABASE_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

BotFather settings

Two things to set once, or features won't work:

  • /setprivacyDisable — otherwise the bot can't see regular messages and can't keep display names fresh
  • /setjoingroupsEnable — on by default, worth confirming

Commands

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

Layout

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

Deploy (Vercel)

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.

The one thing that will confuse you later

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.

Serverless caveats

  • Cold starts add roughly a second to the first ping after an idle spell.
  • Function timeout is 10s on Hobby (vercel.json requests 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: false in src/db/index.ts is required for pgbouncer transaction mode — don't remove it.

Notes

  • Telegram only fires notifications for the first ~5 mentions in a message, so ping.ts batches recipients across multiple messages.
  • Members without a @username are mentioned via tg://user?id= deep links.
  • Polling in dev (src/dev.ts), webhooks in prod (api/telegram.ts). src/bot.ts builds 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.ts catches this and re-points existing huddles. Without it, a group silently loses every huddle the moment it upgrades.

Roadmap

  • Scheduled/recurring pings (/huddle standup every weekday 9am)
  • Auto-prune inactive members from huddles
  • Smart huddle suggestion from message context
  • Per-huddle analytics — response rates, most-pinged

About

Group tooling for Telegram. Opt-in huddles so the right people get pinged — and everyone else doesn't.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages