Edge Adapters
Translate the framework's built server bundle into a form the target runtime can execute. The framework ships two built-in adapters — bun and edge — and stay…
Purpose
Translate the framework's built server bundle into a form the target runtime can execute. The framework ships two built-in adapters — bun and edge — and stays vendor-neutral. Anything vendor-specific (wrangler.toml, vercel.json, deno.json, netlify.toml, deploy commands, vendor binding shapes) lives in a deploy plugin (09-plugins), not here.
Adapter contract
export interface Adapter {
name: "edge" | "bun"
buildTarget: "browser" | "bun" // passed to Bun.build
emit(result: BuildResult, ctx: AdapterContext): Promise<EmittedArtifacts>
}
Note: no deploy() method. Deploying belongs to deploy plugins. Adapters produce portable artifacts; deploy plugins consume them.
edge adapter (default for hosted deployment)
Goal: emit a portable WinterCG / workerd-style Worker module that runs unmodified on any compliant edge runtime — Cloudflare Workers, Deno Deploy, Vercel Edge, Netlify Edge, Bun's edge runtime, and whatever comes next.
- Use
Bun.buildwithtarget: "browser"and Workers/WinterCG conditions set (nonode:*core unless polyfilled). - The Bun target dispatches via
Bun.serve({ routes }), which is unavailable on most edge runtimes. The edge adapter compiles the sameRouteEntry[](from 02b) into a small JS matcher (~30 lines): per-segment match, params extraction, method dispatch. Same handlers, samectx, different entrypoint shape. Conformance with the Bun target is enforced by running the integration fixture set against both. - Wrap the built server entry in the standard Worker module shape: ``
ts export default { async fetch(request: Request, env: Record<string, unknown>, execCtx?: ExecutionContext): Promise<Response> { return matcher.dispatch(request, env, execCtx) } }`Theenvargument is the runtime's bindings object — its shape is vendor-specific, but the framework just forwards it to user code viactx.env`. - Emit
dist/worker.jsanddist/assets/(mirror ofapp/public/plus the built/_patties/client/*chunks). Nowrangler.toml, novercel.json, no vendor config of any kind. - The artifact is portable: the same
dist/worker.jscan be uploaded to multiple hosts. A user with no deploy plugin installed can hand the file towrangler deploy,vercel deploy --prebuilt,deployctl deploy, etc. - Asset serving in production is the host's job. The Worker module does not stream static files — it expects the host to route
/_patties/client/*and/app/public/*paths directly to a CDN / static bucket. Deploy plugins emit the vendor-specific config that makes this happen (e.g. Cloudflare Workers Assets block, VerceloutputDirectory, Netlify[[redirects]], Deno Deploy static-file routing). - Fallback: when no deploy plugin is installed, the Worker's catch-all
fetchhandler streams files from the bundled assets directory using a small ~20-line static reader (path normalize → check map → returnnew Response(file, { headers })). Functional but inefficient — deploy plugins disable it in favor of the host's native asset path.
bun adapter (self-hosted / long-running)
- Emit a
dist/server.jsrunnable viabun dist/server.js. - Static serving uses
Bun.serve's nativestaticroute map — the adapter pre-buildsResponseobjects for every file in./app/publicand./.patties/client(new Response(Bun.file(path), { headers: { ... } })) and passes them asstaticRoutestocreateServer(01). Matched paths never enter JS. The catch-allfetchhandler streams files from disk only as a fallback for paths added at runtime; the static map handles everything known at build time. - Unix sockets and
reusePortare first-class options:patties.config.tsserver: { unix: "/tmp/patties.sock" }orserver: { reusePort: true }plumb straight toBun.serve. Behind nginx, the unix-socket path avoids the loopback hop; withreusePortyou can run N copies for N cores. - Single-binary executable (opt-in): set
adapter.bun.compile: trueinpatties.config.ts. The adapter invokesbun build --compile --target=bun-${platform} --outfile=dist/serverto embed the runtime + bundle. The host machine no longer needsbuninstalled. The binary is ~50MB and takes longer to produce; off by default. - No deploy step in the adapter; document
bun run dist/server.js(or./dist/serverwhen compiled) as the run command. A deploy plugin (e.g.@patties/deploy-bun) may add Dockerfile / systemd / fly.io packaging if the user wants it. - Node is not a supported target — Bun runs everywhere Node does, and supporting Node would require polyfilling
Bun.serve,Bun.Glob,Bun.build, and--watch, which contradicts the Bun-native pillar.
Static assets across targets
| Phase | bun target | edge target |
|---|---|---|
Dev (patties dev) | Bun.serve({ static }) map populated with ./app/public/* and ./.patties/client/* responses | Same as bun — the dev server is always Bun-hosted, regardless of build target |
| Built artifact | Bun.serve({ static }) map; the catch-all fetch handler streams files from disk only as a fallback for paths added at runtime | Host serves dist/assets/ directly; in-process catch-all streaming is the fallback when no deploy plugin reconfigures it |
This means dev parity is held against the bun adapter's behavior. Edge-host idiosyncrasies (cache headers, byte-range support, etc.) show up only after a deploy plugin is involved.
Deploy plugin contract (reference)
Deploy plugins are normal plugins (09) that hook into the build lifecycle:
definePlugin({
name: "deploy-<vendor>",
setup() {}, // no runtime route mounts
hooks: {
onBuildEnd(result) {
// 1. Read result.serverEntry
// 2. Write vendor-specific config (wrangler.toml / vercel.json / etc.) into result.outDir
// 3. Optionally rewrite the worker entry to use vendor bindings
},
},
// Plugins may register CLI commands (RFC pending): `patties deploy` dispatches to the
// first deploy plugin whose `target` matches config.target, or errors if none is present.
})
The framework does not bundle deploy plugins. Users install the one matching their host. Documentation will list community / official deploy plugins; none are blessed as "the reference."
Acceptance criteria
patties build --target edgeproduces adist/worker.jswhoseexport default { fetch }runs on any WinterCG-compliant runtime without modification.patties build --target edgesucceeds with zero deploy plugins installed; the artifact is usable, just without vendor-specific config files.- Installing
@patties/deploy-cloudflareand rebuilding addsdist/wrangler.tomland the Workers Assets config without changing the worker entry's behavior. - The same
dist/worker.jsproduced bypatties build --target edgecan be deployed to two different hosts (e.g. Cloudflare + Vercel Edge) using only their respective deploy plugins — no source changes. patties build --target bunproduces a single executable JS file runnable withbun.patties build --target nodeis rejected at config-validation time with a message naming the allowed targets (bun,edge).