Skip to main content

deployment-hosting

This skill should be used when the user asks about "deploy grammY bot", "webhookCallback", "long polling vs webhooks", "deploy to Cloudflare Workers", "deploy to Vercel", "deploy to Deno Deploy", "Fly.io grammY", "Heroku Telegram bot", "Supabase Edge Functions", "VPS bot deployment", "Firebase functions Telegram", "Zeabur", or needs to put a grammY bot into production.

Source facts

Repository
Agents-Store/codex-plugins
Last source activity
November 1, 2026 at 02:46
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
deployment-hosting
description
This skill should be used when the user asks about "deploy grammY bot", "webhookCallback", "long polling vs webhooks", "deploy to Cloudflare Workers", "deploy to Vercel", "deploy to Deno Deploy", "Fly.io grammY", "Heroku Telegram bot", "Supabase Edge Functions", "VPS bot deployment", "Firebase functions Telegram", "Zeabur", or needs to put a grammY bot into production.
# grammY — Deployment & Hosting Two update-delivery modes (long polling vs webhooks) and ten official hosting recipes. Pick polling for prototypes and tiny bots, webhooks for everything else. ## Long polling vs webhooks | | Long polling (`bot.start()`) | Webhooks (`webhookCallback(bot, …)`) | |---|---|---| | Setup | None — just run | Public HTTPS URL, set webhook once | | Latency | ~0.5–2 s | ~50–200 ms | | Process model | One persistent process | Stateless function per request | | Best for | Development, small bots, VPS | Serverless, edge, autoscaling | | Hosts | VPS, Docker, anywhere with outbound HTTPS | Cloudflare Workers, Vercel, Deno Deploy, Lambda, Cloud Functions, Supabase, Fly, Heroku web dyno | You can run only **one** of them per bot token at a time. Running both gives `409 Conflict`. ## webhookCallback adapter API ```typescript import { webhookCallback } from "grammy"; const handler = webhookCallback(bot, "<adapter>"); ``` Built-in adapters (string names): | Adapter | Framework | |---|---| | `"express"` | Express | | `"fastify"` | Fastify | | `"hono"` | Hono | | `"koa"` | Koa | | `"oak"` | Oak (Deno) | | `"http"` / `"https"` | Node's `http`/`https` — also **Vercel Node.js functions** | | `"std/http"` | Deno `std/http` (and Vercel Edge functions) | | `"serveHttp"` | Deno `serveHttp` request events (`respondWith` style) | | `"cloudflare-mod"` | Cloudflare Workers (modules) | | `"cloudflare"` | Cloudflare Workers (service worker) | | `"aws-lambda"` | AWS Lambda (callback style) | | `"aws-lambda-async"` | AWS Lambda (async handler) | | `"azure"` | Azure Functions (classic `(context, req)` model) | | `"azure-v4"` | Azure Functions (v4 programming model) | | `"sveltekit"` | SvelteKit | | `"next-js"` | Next.js | | `"nhttp"` | nhttp | | `"elysia"` | Elysia | | `"bun"` | Bun.serve | | `"worktop"` | Worktop | | `"callback"` | Bring your own glue: `(update, callback, secretHeader)` | **grammY has no Vercel adapter** — passing the string `vercel` as the adapter is a TypeScript error and fails at runtime. Vercel Node.js functions use `"https"`; Vercel Edge functions use `"std/http"` (see the Vercel recipe below). If your framework isn't in the list, the `"http"` adapter works for any Node HTTP server. `webhookCallback(bot, adapter, options?)` takes an optional third argument: `{ secretToken, onTimeout, timeoutMilliseconds }`. `secretToken` makes grammY verify the `X-Telegram-Bot-Api-Secret-Token` header for you; `timeoutMilliseconds` defaults to 10 000 and `onTimeout` to `"throw"` (`"return"` answers the request early instead). ## Setting the webhook URL After deploying, tell Telegram where to call you: ```bash curl "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook?url=https://your-domain.com/<BOT_TOKEN>&drop_pending_updates=true" ``` Or from code: ```typescript await bot.api.setWebhook("https://your-domain.com/<BOT_TOKEN>", { drop_pending_updates: true, secret_token: process.env.WEBHOOK_SECRET, }); ``` Setting `secret_token` makes Telegram send an `X-Telegram-Bot-Api-Secret-Token` header. You do not have to check it by hand — pass the same value to `webhookCallback` and grammY rejects mismatching requests with `401`: ```typescript const handler = webhookCallback(bot, "express", { secretToken: process.env.WEBHOOK_SECRET, }); ``` ## Recipe: VPS with Express + webhooks ```typescript import express from "express"; import { webhookCallback } from "grammy"; import { bot } from "./bot.js"; const app = express(); app.use(express.json()); const secretPath = String(process.env.BOT_TOKEN); const secretToken = process.env.WEBHOOK_SECRET; app.use(`/${secretPath}`, webhookCallback(bot, "express", { secretToken })); app.listen(Number(process.env.PORT) || 3000, async () => { await bot.api.setWebhook(`https://${process.env.DOMAIN}/${secretPath}`, { secret_token: secretToken, }); }); ``` Pair with nginx for TLS termination, or use Caddy for automatic HTTPS. ## Recipe: VPS with Fastify ```typescript import { fastify } from "fastify"; import { webhookCallback } from "grammy"; import { bot } from "./bot.js"; const server = fastify(); server.post(`/${bot.token}`, webhookCallback(bot, "fastify")); await server.listen({ port: 3000, host: "0.0.0.0" }); ``` ## Recipe: Cloudflare Workers (TypeScript modules) ```typescript import { Bot, Context, webhookCallback } from "grammy"; export interface Env { BOT_TOKEN: string; BOT_INFO: string; // JSON.stringify(await bot.api.getMe()) cached } export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { const bot = new Bot(env.BOT_TOKEN, { botInfo: JSON.parse(env.BOT_INFO) }); bot.command("start", (ctx: Context) => ctx.reply("Hello, world!")); return webhookCallback(bot, "cloudflare-mod")(request); }, }; ``` `BOT_INFO` skips a `getMe()` round-trip on every cold start. Generate it once: ```bash curl "https://api.telegram.org/bot$BOT_TOKEN/getMe" # paste the .result object as the BOT_INFO env var ``` Set the env vars with `wrangler secret put BOT_TOKEN` and `wrangler secret put BOT_INFO`. ## Recipe: Cloudflare Workers (Deno) ```typescript import { Bot, webhookCallback } from "npm:grammy"; import type { UserFromGetMe } from "npm:grammy/types"; interface Env { BOT_TOKEN: string } let botInfo: UserFromGetMe | undefined; export default { async fetch(request: Request, env: Env) { const bot = new Bot(env.BOT_TOKEN, { botInfo }); if (botInfo === undefined) { await bot.init(); botInfo = bot.botInfo; } bot.command("start", (ctx) => ctx.reply("Up and running.")); bot.on("message", (ctx) => ctx.reply("Got it.")); return webhookCallback(bot, "cloudflare-mod")(request); }, }; ``` ## Recipe: Vercel (Node.js) `/api/bot.ts` — Vercel Node.js functions receive Node's `(req, res)` pair, so use the `"https"` adapter: ```typescript import { Bot, webhookCallback } from "grammy"; const token = process.env.BOT_TOKEN; if (!token) throw new Error("BOT_TOKEN is unset"); const bot = new Bot(token); bot.command("start", (ctx) => ctx.reply("Hello from Vercel")); export default webhookCallback(bot, "https", { secretToken: process.env.WEBHOOK_SECRET, }); ``` Edge functions are a different runtime: add `export const config = { runtime: "edge" };` and switch the adapter to `"std/http"`. Only the core package and edge-compatible plugins work there — test every plugin you use. After `vercel deploy`, set the webhook: ```text https://api.telegram.org/bot<BOT_TOKEN>/setWebhook?url=https://<your-app>.vercel.app/api/bot ``` ## Recipe: Deno Deploy ```typescript // main.ts import { Bot, webhookCallback } from "npm:grammy"; const bot = new Bot(Deno.env.get("BOT_TOKEN")!); bot.command("start", (ctx) => ctx.reply("Hi from Deno Deploy")); const handler = webhookCallback(bot, "std/http"); Deno.serve(handler); ``` Deno Deploy Classic (the old dashboard and `deployctl`) was shut down on 2026-07-20. Create an organization in the new Deno Deploy console, connect the repository or run `deno deploy`, and keep `Deno.serve` (the old `std/http` `serve()` is not supported there). Classic projects are not migrated automatically — see the Deno Deploy migration guide in the Deno docs. ## Recipe: Supabase Edge Functions ```typescript // supabase/functions/bot/index.ts import { Bot, webhookCallback } from "npm:grammy"; const bot = new Bot(Deno.env.get("BOT_TOKEN")!); bot.command("start", (ctx) => ctx.reply("Hi from Supabase")); Deno.serve(webhookCallback(bot, "std/http")); ``` Deploy with `supabase functions deploy bot --no-verify-jwt`, then point Telegram to `https://<project-ref>.functions.supabase.co/bot`. ## Recipe: Fly.io (Docker + long polling or webhooks) A `Dockerfile` for a polling bot: ```dockerfile FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY . . CMD ["node", "src/bot.js"] ``` Node 18 and 20 are end-of-life — use `node:22-alpine` or `node:24-alpine`. Then `fly launch`. Fly auto-scales horizontally — switch to webhooks once you have more than one machine. ## Recipe: Heroku (Express + webhooks) `web` dyno in `Procfile`: ```text web: node src/server.js ``` `src/server.js`: ```typescript import express from "express"; import { webhookCallback } from "grammy"; import { bot } from "./bot.js"; const app = express(); app.use(express.json()); app.use(`/${process.env.BOT_TOKEN}`, webhookCallback(bot, "express")); app.listen(process.env.PORT, async () => { await bot.api.setWebhook(`https://${process.env.HEROKU_APP_NAME}.herokuapp.com/${process.env.BOT_TOKEN}`); }); ``` ## Recipe: Firebase Functions ```typescript import { onRequest } from "firebase-functions/v2/https"; import { Bot, webhookCallback } from "grammy"; const bot = new Bot(process.env.BOT_TOKEN!); bot.command("start", (ctx) => ctx.reply("Hi from Firebase")); export const telegram = onRequest(webhookCallback(bot, "express")); ``` ## Recipe: Zeabur `Dockerfile` or `package.json` start script — Zeabur autodetects Node/Deno. Use polling for simplicity; switch to webhooks when you need multi-region. ## Choosing a platform | Constraint | Pick | |---|---| | Lowest cost, single-region, instant cold starts | Cloudflare Workers | | Always-on, paid plan, autoscaling | Fly.io | | Already on Vercel for the rest of your stack | Vercel | | Native Deno, Web standard APIs | Deno Deploy or Supabase | | Self-hosted, full control | VPS + Docker + nginx | | Mobile / startup, free tier ok | Cloudflare Workers (free 100k req/day) | ## Bundled helper This plugin ships a script that emits the right webhook adapter for your platform: ```bash ${CLAUDE_PLUGIN_ROOT}/scripts/generate-webhook-adapter.sh <framework> <out-file> # framework ∈ {express, fastify, hono, cloudflare, https} (vercel = alias of https) ``` The generated adapters read an optional `WEBHOOK_SECRET` and pass it as `secretToken`. ## Debug a deploy After setting the webhook, query Telegram for status: ```bash curl "https://api.telegram.org/bot$BOT_TOKEN/getWebhookInfo" | jq ``` Fields to watch: - `last_error_date` / `last_error_message` — Telegram's view of *why* your endpoint failed - `pending_update_count` — backlog (should be 0) - `url` — what Telegram thinks your URL is If you see `409 Conflict`, you're running `bot.start()` somewhere else. Stop it.
View on GitHub