@yaebal/web
run your bot on any runtime via webhooks — edge (cloudflare, deno, bun, vercel), node servers (express, fastify, koa), serverless (aws lambda, azure, google cloud functions), and fetch frameworks (hono, elysia, next.js, sveltekit)
install
pnpm add @yaebal/webusage
no long-polling: each incoming Request becomes one update. webhook(bot, options?) returns a standard (Request, execution?) => Promise<Response> that drops straight into any
fetch runtime. the optional second argument is the platform context (cloudflare's ctx) — pass it so a slow update can finish via waitUntil.
import { Bot } from "@yaebal/core";
import { webhook } from "@yaebal/web";
// cloudflare workers / deno deploy / vercel edge / bun — no long-polling, just fetch
export default {
fetch(request: Request, env: { BOT_TOKEN: string; SECRET: string }, ctx) {
const bot = getBot(env); // build once per isolate, not per request
return webhook(bot, { secretToken: env.SECRET })(request, ctx);
},
};pass botInfo to the Bot constructor on serverless to skip the getMe cold-start round trip; otherwise the handler resolves it lazily on the first
update, so ctx.me and /cmd@botname addressing work without start().
framework adapters
for frameworks whose handler shape isn't (Request) => Response, an adapter wraps
the same handler. every adapter forwards the secret token and request path, so secretToken and path behave identically across all of them — and each
is zero-node:-import (node bodies are read through the stream interface).
import { expressAdapter, fastifyAdapter, honoAdapter, awsLambdaAdapter } from "@yaebal/web";
// express / google cloud functions / firebase
app.post("/", expressAdapter(bot, { secretToken: SECRET }));
// fastify
fastify.post("/", fastifyAdapter(bot, { secretToken: SECRET }));
// hono — also threads c.executionCtx.waitUntil
app.post("/", honoAdapter(bot, { secretToken: SECRET }));
// aws lambda (api gateway / function url)
export const handler = awsLambdaAdapter(bot, { secretToken: SECRET });| adapter | runtime / framework |
|---|---|
honoAdapter | hono — wires waitUntil |
elysiaAdapter | elysia |
cloudflareAdapter | cloudflare workers module syntax (request, env, ctx) |
nextAdapter | next.js app-router route handler (remix, astro too) |
svelteKitAdapter | sveltekit endpoint — reads platform.context |
expressAdapter / gcfAdapter | express, google cloud functions, firebase |
fastifyAdapter | fastify |
koaAdapter | koa |
awsLambdaAdapter | aws lambda behind api gateway / function url |
azureAdapter | azure functions (v3 model) |
all of them are also grouped on the adapters object — adapters.express(bot).
standalone server (node / bun / deno)
serve(bot, options?) starts the runtime's native http server and resolves to a
handle you can stop(). it works on node too (lazily importing node:http), so there's no per-runtime special-casing. on an edge platform with no
server to own, export { fetch: webhook(bot) } instead.
import { serve } from "@yaebal/web";
// node, bun, or deno — the native http server, no per-runtime branching
const server = await serve(bot, { port: 8080, secretToken: process.env.SECRET });
process.once("SIGINT", () => server.stop());
console.log(`listening on ${server.url}`);production hardening
telegram fires webhook updates in parallel (up to maxConnections) and redelivers on
failure. these two combinators are what a serious deployment needs:
import { sequentialize, dedupe } from "@yaebal/web";
// telegram delivers webhook updates in parallel — order them per chat so
// sessions/scenes/conversations don't clobber each other. install this FIRST.
bot.use(sequentialize());
// drop the updates telegram redelivers when a request fails or times out.
bot.use(dedupe());for defence in depth, isTelegramIp(ip) checks the peer against telegram's published
subnets — pass the client ip your platform exposes
(cf-connecting-ip, x-forwarded-for, req.ip).
webhook options
webhook(bot, {
secretToken: SECRET, // require the X-Telegram-Bot-Api-Secret-Token header
path: "/telegram", // only serve this path; others hit fallback / 404
fallback: () => new Response("ok"), // health checks, GET probes
timeoutMs: 10_000, // answer anyway after 10s so telegram won't redeliver
onTimeout: "ack", // "ack" (200, keep running) | "fail" (500, redeliver)
onError: "fail", // "fail" (500, redeliver) | "ack" (200, drop)
maxBodyBytes: 1 << 20, // streaming-enforced body cap (1 MiB default)
reply: true, // answer the webhook request with an api call (saves a round trip)
});register & diagnose
tell telegram where to send updates. the secretToken you set here must match the one
you pass to webhook() / serve() — it's validated locally and checked on
every request.
import { setWebhook, getWebhookInfo, deleteWebhook } from "@yaebal/web";
// run once on deploy to point telegram at your url
await setWebhook(bot, "https://my-worker.workers.dev/", {
secretToken: process.env.SECRET,
allowedUpdates: ["message", "callback_query"],
dropPendingUpdates: true,
maxConnections: 40,
});
// diagnose stalled deliveries — pending_update_count, last_error_message, …
const info = await getWebhookInfo(bot);
// switch back to long-polling later
await deleteWebhook(bot, { dropPendingUpdates: true });api
| export | description |
|---|---|
webhook(bot, options?) | fetch handler (Request, execution?) => Promise<Response> for edge/web runtimes |
serve(bot, options?) | standalone http server on node/bun/deno; resolves to { url, port, stop() } |
honoAdapter, expressAdapter, … | per-framework handlers (see table above); also on the adapters object |
sequentialize(key?) | middleware: order updates per chat (or custom key) |
dedupe(options?) | middleware: drop redelivered update_ids |
isTelegramIp(ip) | is the peer in a telegram webhook subnet? |
setWebhook(bot, url, options?) | register the webhook (secret, allowed updates, ip, certificate, …) |
deleteWebhook(bot, options?) | remove the webhook; accepts { dropPendingUpdates } or a bare boolean |
getWebhookInfo(bot) | current status: pending count, last error, url |
serve() runs everywhere; the package is fetch-first with zero static node: imports, so it still bundles
for edge. for a hand-rolled node server you can also use nodeWebhookCallback from @yaebal/core/node. the operator dashboard lives in @yaebal/panel.