A Telegram-driven team task, reminder, and leave-management service.
Roster keeps a small team accountable without anyone living in a dashboard: tasks and reminders are pushed to each member over Telegram, completion is a single inline-button tap, and the system understands recurring schedules, multi-assignee completion rules, and approved leave.
Supervisor ──creates──▶ Task (recurring, multi-assignee, importance)
│
scheduler scans every minute
│ enqueues per-user reminder jobs (lead-time aware,
▼ suppressed while the assignee is on approved leave)
Member's Telegram ◀──────┘ "❗ Stand-up at 09:30 [✅ Mark complete]"
│ taps button
▼
Assignment COMPLETED ──▶ ANY mode: task done, peers told to stand down
ALL mode: task done once everyone has reported
- Tasks & reminders with an
importanceflag. - Recurrence: weekly (by weekdays), monthly-by-date, monthly-by-nth-weekday (incl. last weekday). Occurrences preserve the base clock time.
- Multi-assignee completion semantics —
ANY(first finisher closes it and peers are told to stand down) orALL(everyone must report). - Leave management — request → supervisor approve/reject; assignments during approved leave are auto-skipped and reminders suppressed.
- Telegram bot — account binding via one-shot deep-link token, reminder push, inline "Mark complete" buttons, and a daily roll-up review.
- Scheduling — per-minute reminder scan + 5-minute overdue scan + a timezone-aware daily-review cron, with the heavy per-user work fanned out over a BullMQ queue (deterministic job ids prevent double-sends).
- GraphQL API for everything a frontend needs.
A clean, layered NestJS application:
flowchart TB
client(["Web client"])
tguser(["Telegram user"])
subgraph edge["Edge"]
api["GraphQL API<br/>(roster.resolver)"]
wh["Telegram webhook<br/>(constant-time secret, fail-closed)"]
end
subgraph services["Application services"]
svc["TaskService · LeaveService<br/>ReminderService · DailyReviewService<br/>TelegramService"]
end
subgraph domain["Domain — pure, unit-tested (23 tests)"]
dom["recurrence · completion · validation<br/>leave · daily-review"]
end
repos["Repositories<br/>(Prisma, @Transactional)"]
pg[("PostgreSQL")]
subgraph async["Async work"]
sched["Scheduler (@Cron)<br/>per-min reminder · 5-min overdue · daily review"]
queue[["BullMQ queue · Redis"]]
proc["Queue processor"]
end
tgapi(["Telegram Bot API"])
client -->|"GraphQL + JWT"| api
tguser -->|"webhook"| wh
api --> svc
wh --> svc
svc --> dom
svc --> repos --> pg
sched --> svc
svc -->|"enqueue"| queue --> proc --> tgapi
proc --> repos
| Layer | Path | Responsibility |
|---|---|---|
| Domain (pure, framework-free) | src/domain/ |
Recurrence expansion, completion rules, leave overlap, validation, daily-review composition. No NestJS, no Prisma — unit-tested in isolation. |
| Data | src/roster/*.repository.ts |
Prisma repositories over a transaction-aware client (@Transactional() via nestjs-cls). |
| Services | src/roster/*.service.ts |
Orchestration: tasks, leave, Telegram, reminder scan, daily review. |
| Edge | roster.resolver.ts, telegram-webhook.controller.ts |
GraphQL API + the Telegram webhook (constant-time secret check, fail-closed in production). |
| Infra | src/{config,prisma,auth,queue}/ |
Zod-validated config, Prisma, minimal JWT auth, BullMQ queue + scheduler. |
The pure domain layer is the heart of the system and carries the test suite — everything else is wiring around it.
# 1. Dependencies (Postgres + Redis)
docker compose up -d
# 2. Configure
cp .env.example .env # the defaults match docker-compose
# 3. Install, migrate, seed
npm install
npm run prisma:migrate # creates the schema
npm run db:seed # demo users (password: password123)
# 4. Run
npm run start:dev # http://localhost:3010/graphqlLog in to get a token:
curl -s localhost:3010/auth/login \
-H 'content-type: application/json' \
-d '{"email":"supervisor@roster.local","password":"password123"}'
# → { "accessToken": "ey…", "user": { … } }Then call GraphQL with Authorization: Bearer <accessToken>:
query { health { ok enabled } }
mutation {
createTask(input: {
title: "Morning stand-up"
type: TASK
completionMode: ANY
importance: true
startAt: "2026-07-01T09:30:00Z"
recurrence: { kind: WEEKLY, byWeekday: [1,2,3,4,5] }
assigneeIds: ["<member-id>"]
}) { id status }
}Roster runs fine without Telegram (you just won't get push notifications). To
enable it, create a bot via @BotFather, set in .env:
TELEGRAM_ENABLED=true
TELEGRAM_BOT_TOKEN=123456:AA...
TELEGRAM_BOT_USERNAME=your_bot
TELEGRAM_WEBHOOK_SECRET=$(openssl rand -hex 24) # required in productionPoint Telegram at your public URL and register the webhook:
curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
-d "url=https://<your-host>/api/telegram/webhook" \
-d "secret_token=$TELEGRAM_WEBHOOK_SECRET"Users bind their account from startTelegramBind (returns a t.me deep link),
then receive reminders and tap Mark complete.
The pure domain logic is fully unit-tested (recurrence math, completion rules, validation) — no database required:
npm testNestJS · GraphQL (Apollo, code-first) · Prisma + PostgreSQL · BullMQ + Redis ·
@nestjs/schedule · nestjs-cls transactions · Zod · Vitest · TypeScript.
Roster began as a module I built on top of AFFiNE
for a self-hosted deployment. This repository is a standalone reimplementation
of that module — it contains none of AFFiNE's source; the domain logic, data
model, API, Telegram integration, and scheduling were re-homed into the clean,
dependency-light service you see here. See NOTICE.md.
MIT.