runtime support

yaebal is fetch-first and has zero native-module dependencies in the core, so Bun, Deno, and edge runtimes like Cloudflare Workers work out of the box. This page lists what runs where.

support matrix

featureNodeBunDenoCF Workers / edge
minimum version≥20 (18+ for fetch, but the repo targets 20)latestlatestn/a — managed by the platform
@yaebal/core + most plugins
media.path() (local files)✅ (needs --allow-read)❌ no filesystem — use media.url() / media.buffer()
@yaebal/webwebhook()
@yaebal/core/nodenodeWebhookCallback✅ (compat)✅ (compat)❌ no node:http
@yaebal/sklad storage adapters✅ all✅ all✅ all✅ memory/KV-backed only — anything wrapping node:fs needs a custom adapter
@yaebal/panel main handler✅ fetch handler
@yaebal/panel/sqlite✅ Node ≥22.5
@yaebal/panel/sklad (skladPanelStore)✅ any adapter✅ any adapter✅ any adapter✅ with an edge-safe adapter (kvStorage, redisStorage, MemoryStorage) — sqliteStorage still needs node:sqlite
@yaebal/router (file-based)❌ needs fs
@yaebal/workers (thread pool)⚠️ limited❌ no worker_threads
@yaebal/cron persisted store✅ any @yaebal/sklad adapter✅ with a KV-backed store; timers still run only while the isolate is alive
create-yaebal (CLI)N/A
why fetch-first? The Fetch API is the one HTTP primitive shared by Node 18+, Bun, Deno, and all edge runtimes. By building on (Request) => Promise<Response> instead of (req, res), yaebal adapters work everywhere without shims.

Cloudflare Workers

webhook() from @yaebal/web returns a standard fetch handler. Workers only give you env inside the fetch() export, so construct the bot there rather than at module scope — a top-level new Bot(env.BOT_TOKEN) throws because env doesn't exist yet when the module loads.

worker.ts
// Cloudflare Workers — fetch-first, runs at the edge
import { Bot } from "@yaebal/core";
import { webhook } from "@yaebal/web";

interface Env {
  BOT_TOKEN: string;
}

export default {
  fetch(request: Request, env: Env) {
    // create the bot per-request: Workers give you "env" only inside fetch(),
    // never at module scope.
    const bot = new Bot(env.BOT_TOKEN);
    bot.command("start", (ctx) => ctx.reply("hi from the edge"));

    return webhook(bot)(request);
  },
};

Deno Deploy and Vercel Edge follow the same shape: a fetch handler, no Node globals.

Bun

Pass the same fetch handler to Bun.serve. Bun's Node compatibility layer means you can also use nodeWebhookCallback with Bun.serve's node:http-style API if you prefer, but the fetch path is simpler.

server.ts
// Bun — built-in HTTP server
import { Bot } from "@yaebal/core";
import { webhook } from "@yaebal/web";

const bot = new Bot(process.env.BOT_TOKEN!);
bot.on("message:text", (ctx) => ctx.reply(ctx.text));

Bun.serve({ port: 8080, fetch: webhook(bot) });

Deno

Deno.serve accepts a fetch handler directly. Import from npm via Deno's npm specifier support (npm:@yaebal/core) or use a local build.

server.ts
// Deno — built-in HTTP server
import { Bot } from "@yaebal/core";
import { webhook } from "@yaebal/web";

declare const Deno: {
  env: { get(key: string): string | undefined };
  serve(options: { port: number }, handler: (request: Request) => Response | Promise<Response>): unknown;
};

const bot = new Bot(Deno.env.get("BOT_TOKEN")!);
bot.on("message:text", (ctx) => ctx.reply(ctx.text));

Deno.serve({ port: 8080 }, webhook(bot));

edge limitations

Edge runtimes (Cloudflare Workers, Deno Deploy, Vercel Edge) don't expose the filesystem or worker_threads, so anything that relies on those won't run there:

  • media.path() — reads local files off disk; use media.url() for a public URL or media.buffer() for in-memory bytes instead. See media & files.
  • @yaebal/panel/sqlite — optional SQLite store; uses node:sqlite and requires Node ≥22.5. the main @yaebal/panel fetch handler runs fine on edge — pair it with skladPanelStore from @yaebal/panel/sklad and a KV/Redis/memory sklad adapter instead of sqlite.
  • @yaebal/router — discovers handler files at startup; needs fs
  • @yaebal/workers — spawns a worker_threads pool; not available at the edge
  • @yaebal/core/node — uses node:http; keep it out of edge bundles.

Everything else — core, all filter/keyboard/session/i18n plugins, webhook() — runs fine at the edge.