CORE CONCEPTS

Runtime Adapters

Futon itself never opens a port. app.fetch(request) just turns a Request into a Response. @rasenganjs/runtime is the companion package that actually binds an HTTP server, for each supported platform.

What each package owns
@rasenganjs/futon → the request pipeline (router, middleware, ctx) @rasenganjs/runtime → binding a real server around that pipeline

Node

Node development server
import { Futon } from '@rasenganjs/futon'; import { NodeDevAdapter } from '@rasenganjs/runtime/adapters/node'; const app = new Futon(); app.get('/hello', async () => new Response('Hello!')); const adapter = new NodeDevAdapter({ port: 3000 }); await adapter.serve(app);

@rasenganjs/runtime/adapters/node exports both NodeDevAdapter (file watching, auto-restart) and NodeProdAdapter, plus lower-level pieces (NodeAssets, NodeWatcher, startNodeServer, loadNodeEnvFiles) if you need finer control than the adapter classes provide.

Bun

Bun
import { Futon } from '@rasenganjs/futon'; import { BunDevAdapter } from '@rasenganjs/runtime/adapters/bun'; const app = new Futon(); // ... routes const adapter = new BunDevAdapter({ port: 3000 }); await adapter.serve(app);

Cloudflare Workers (workerd)

Workers don't have a long-running dev server process in the same sense. Deployment goes through @rasenganjs/runtime/adapters/workerd's production adapter, paired with toWinterCgHandler() for the actual fetch export.

Choosing the Right Adapter

You're buildingUse
A standalone Node app/APINodeDevAdapter / NodeProdAdapter
A standalone Bun app/APIBunDevAdapter / BunProdAdapter
A Cloudflare WorkertoWinterCgHandler() + the workerd production adapter
Mounting inside existing Express routestoExpressHandler()
A full backend with controllers + DI@rasenganjs/server, which wraps these adapters for you

RuntimeContext and ServerInfo

Every adapter populates ctx.runtime.server before your first request, so handlers can introspect how they're being served:

Reading server info
app.get('/debug', async (ctx) => { return json({ preset: ctx.runtime.server?.preset, // 'node' | 'bun' | 'workerd' | 'express' | 'wintercg' mode: ctx.runtime.server?.mode, // 'development' | 'production' port: ctx.runtime.server?.port, }); });

Environment Variables

Futon itself is zero-dependency and never touches the filesystem. Reading .env* files is the Node/Bun adapters' job, not Futon's. NodeDevAdapter, NodeProdAdapter, BunDevAdapter, and BunProdAdapter all call loadNodeEnvFiles/loadBunEnvFiles before .serve() returns:

What each adapter does before serving
import { loadNodeEnvFiles } from '@rasenganjs/runtime/adapters/node'; const vars = await loadNodeEnvFiles(rootDir, 'development'); // or 'production' app.loadEnv(vars);

This does two things:

  • Assigns every loaded key into process.env, skipping any key already set there. A real environment variable (shell, CI, a platform secret) always wins over a .env* file's value.
  • Feeds the same values into app.env / ctx.runtime.env, the portable access path that works the same way across every runtime Futon supports.
Reading env inside a handler
app.get('/debug', async (ctx) => { return json({ fromProcessEnv: process.env.DATABASE_URL, // Node/Bun only fromRuntimeContext: ctx.runtime.env?.DATABASE_URL, // works everywhere, including workerd bindings }); });

Four filenames are checked, in this order, later files overriding earlier ones for the same key: .env.env.local.env.{mode}.env.{mode}.local.

WinterCG Adapter
Lazy Request