Files
miti99bot/plans/reports/researcher-260411-0853-grammy-on-cloudflare-workers.md
tiennm99 c4314f21df feat: scaffold plug-n-play telegram bot on cloudflare workers
grammY-based bot with a module plugin system loaded from the MODULES env
var. Three command visibility levels (public/protected/private) share a
unified command namespace with conflict detection at registry build.

- 4 initial modules (util, wordle, loldle, misc); util fully implemented,
  others are stubs proving the plugin system end-to-end
- util: /info (chat/thread/sender ids) + /help (pure renderer over the
  registry, HTML parse mode, escapes user-influenced strings)
- KVStore interface with CFKVStore and a per-module prefixing factory;
  getJSON/putJSON convenience helpers; other backends drop in via one file
- Webhook at POST /webhook with secret-token validation via grammY's
  webhookCallback; no admin HTTP surface
- Post-deploy register script (npm run deploy = wrangler deploy && node
  --env-file=.env.deploy scripts/register.js) for setWebhook and
  setMyCommands; --dry-run flag for preview
- 56 vitest unit tests across 7 suites covering registry, db wrapper,
  dispatcher, help renderer, validators, and HTML escaper
- biome for lint + format; phased implementation plan under plans/
2026-04-11 09:49:06 +07:00

2.8 KiB

Researcher Report: grammY on Cloudflare Workers

Date: 2026-04-11 Scope: grammY entry point, webhook adapter, secret-token verification, setMyCommands usage.

Key findings

Adapter

  • Use "cloudflare-mod" adapter for ES module (fetch handler) Workers. Source: grammY src/convenience/frameworks.ts.
  • The legacy "cloudflare" adapter targets service-worker style Workers. Do NOT use — CF has moved on to module workers.
  • Import path (npm, not Deno): import { Bot, webhookCallback } from "grammy";

Minimal fetch handler

import { Bot, webhookCallback } from "grammy";

export default {
  async fetch(request, env, ctx) {
    const bot = new Bot(env.TELEGRAM_BOT_TOKEN);
    // ... register handlers
    const handle = webhookCallback(bot, "cloudflare-mod", {
      secretToken: env.TELEGRAM_WEBHOOK_SECRET,
    });
    return handle(request);
  },
};

Secret-token verification

  • webhookCallback accepts secretToken in its WebhookOptions. When set, grammY validates the incoming X-Telegram-Bot-Api-Secret-Token header and rejects mismatches with 401.
  • No need to manually read the header — delegate to grammY.
  • The same secret must be passed to Telegram when calling setWebhook (secret_token field).

Bot instantiation cost

  • new Bot() per request is acceptable for Workers (no persistent state between requests anyway). Global-scope instantiation also works and caches across warm invocations. Prefer global-scope for reuse but be aware env bindings are not available at module load — must instantiate lazily inside fetch. Recommended pattern: memoize Bot in a module-scope variable initialized on first request.

setMyCommands

  • Call via bot.api.setMyCommands([{ command, description }, ...]).
  • Should be called on demand, not on every webhook request (rate-limit risk, latency). Two options:
    1. Dedicated admin HTTP route (e.g. POST /admin/setup) guarded by a second secret. Runs on demand.
    2. One-shot wrangler script. Adds tooling complexity.
  • Recommendation: admin route. Keeps deploy flow in one place (wrangler deploy + curl). No extra script.

Init flow

  • bot.init() is NOT required if you only use webhookCallback; grammY handles lazy init.
  • For /admin/setup that directly calls bot.api.*, call await bot.init() once to populate bot.botInfo.

Resolved technical answers

Question Answer
Adapter string "cloudflare-mod"
Import import { Bot, webhookCallback } from "grammy"
Secret verify pass secretToken in webhookCallback options
setMyCommands trigger admin HTTP route guarded by separate secret

Unresolved questions

  • None blocking. grammY version pin: recommend ^1.30.0 or latest stable at implementation time; phase-01 should npm view grammy version to confirm.