This file provides guidance to Claude Code (claude.ai/code) when working in this repository.
A GrammY-based Telegram bot serving KTU (Kerala Technological University) students. The system consists of a long-polling bot and several BullMQ background workers, all orchestrated via Docker Compose. PostgreSQL (Drizzle ORM) for persistence, Redis (BullMQ) for job queues.
docker compose -f docker/compose/compose.dev.yaml up --buildThis starts all services (bot, workers, PostgreSQL, Redis, Bull Board on :3010) with hot-reload.
For detailed architecture, data flow, and worker responsibilities, read docs/working.md. Do not repeat that information here — refer to it on demand when you need deeper context.
- Pure ESM (
"type": "module"in package.json). - Module resolution:
NodeNext. All local imports MUST use.jsextensions even though source files are.ts:import { BotError } from "../errors/bot-errors.js";
- Third-party imports do NOT use
.js.
tsconfig.json enables full strict mode plus:
noUncheckedIndexedAccess— all indexed access includesundefined.exactOptionalPropertyTypes—{ key?: string }forbids passingundefined.noUnusedLocals,noUnusedParameters— unused variables are errors. Prefix with_to suppress.noImplicitOverride— must useoverridekeyword on class overrides.noImplicitReturns— all code paths must return.
- ESLint: Flat config with
typescript-eslinttype-checked rules.no-floating-promises: error,no-unused-vars(with_prefix exception). anyis strictly banned. Never useany. Type everything properly — useunknownif the type is truly unknown and narrow it with type guards.- Formatting strings: Use GrammY's
fmttemplate literal tag (from@grammyjs/parse-mode). UsejoinWithNewlines()for multi-line messages. - Prettier is enforced by the pre-commit hook — you don't need to worry about formatting.
- Naming: Files/dirs use
kebab-case. FunctionscamelCase. Classes/interfacesPascalCase. Exported configsPascalCase. - Barrel exports: Every directory has an
index.tsre-exporting all public members.
All environment variables are validated at startup using Zod v4 schemas in src/configs/. The pattern:
const schema = z.object({ ... });
export const Config = schema.parse({ ENV_VAR: process.env.ENV_VAR, ... });Use .superRefine() for cross-field validation and .transform() for derived values. Never access process.env directly outside of config modules.
BotError(base class): carriesuserMessage— the safe-to-show-user message.KTUAPIError extends BotError: API errors withstatusCode,url,serviceName.SessionNotFoundError extends BotError: Session expired, has a default user-friendly message.HandledBotError: Wraps an error that was already handled by a composer error boundary. Prevent double-notification.
Error handling flow:
- Composer error boundaries catch errors → clean up loading messages from session → notify user → wrap in
HandledBotErrorand re-throw. - Global error handler catches everything. If it's a
HandledBotError, it skips (already notified). Otherwise sends a generic message.
When writing new composers, always apply: .errorBoundary(createComposerErrorBoundary([...sessionKeys])) on the composer handling callback queries.
BotContext extends GrammY's Context with HydrateFlavor, CommandsFlavor, EmojiFlavor, and SessionFlavor<SessionData>. Session stores pagination state and message IDs for cleanup. Always type session keys with keyof SessionData.
Before making any changes involving GrammY APIs, plugins, or patterns, look up the relevant documentation first:
- Main docs: https://grammy.dev
- Use
context7_query-docswith library ID/grammyjs/grammYfor code examples. - Use
deepwiki_ask_questionwith repogrammyjs/grammYfor questions.
Common GrammY patterns in this codebase:
- Composers: Each feature is a
Composer<BotContext>mounted insrc/bot/bot.ts. Use@grammyjs/commandsfor command groups. - Plugins: The project uses
auto-retry,commands,emoji,hydrate,parse-mode,ratelimiter,runner,transformer-throttler. Checkpackage.jsonfor versions before adding new plugins. - API calls: Always use
ctx.api(the auto-retry-aware instance). For workers (no middleware), use the rawbot.apifrom a worker bot created viacreateWorkerBot(). - Media groups: Max 10 files per
sendMediaGroupcall. Caption only on the first item of the first batch. - Rate limiting: Telegram rate limits are undocumented. The broadcasts worker uses
concurrency: 1to handle them safely. When you hit aretry_aftererror, pause the queue and re-throw for BullMQ retry.