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.
@rasenganjs/futon → the request pipeline (router, middleware, ctx) @rasenganjs/runtime → binding a real server around that pipeline
Node
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
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
If you're using
@rasenganjs/server, the
preset field in rasengan.server.ts ('node' | 'bun' | 'workerd') selects
the right runtime adapter automatically — this page is most relevant if you're
using Futon standalone.
RuntimeContext and ServerInfo
Every adapter populates ctx.runtime.server before your first request, so handlers can introspect how they're being served:
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, }); });
