Mixed-runtime split
A single Heximon app can deploy its controllers across two runtimes in one deployment: a read-heavy endpoint
on a fast, global edge function and a write endpoint on a node function where the database lives. You mark
one controller runtime: "edge"; the compiler does the rest.
This is something a route-rules-on-a-shared-bundle model cannot do. Nitro's vercel.functionRules split
per-route function config (memory, regions) but symlink one shared bundle — every route has the same dependency
closure by construction, so the runtime can't differ. Heximon's compile-time DI graph lets each tier carry its
own closure, and — the real payoff — proves the edge tier's closure is edge-safe before you deploy.
Opt in on the controller
runtime is a field on the controller config — the same type argument that already carries prefix /
middlewares / errorFilters / scopes. The default is node; one field opts a controller, and its whole
transitive dependency closure, onto the edge tier:
import type { Controller, Get } from "@heximon/http";
import { IdService } from "../shared/id-service";
export class FeedController implements Controller<{ prefix: "/feed"; runtime: "edge" }> {
public constructor(private readonly ids: IdService) {}
public async list(_action: Get<"/">): Promise<readonly FeedItem[]> {
// … edge-safe, read-only work …
}
}
A controller with no runtime (or runtime: "node") stays on the node tier — the existing behavior, unchanged.
A hybrid deploy target
The split lights up only on a hybrid target — one that owns both a node and an edge tier in one deployment.
Set it on heximon.platform:
import { defineHeximonConfig } from "@heximon/build";
import { HttpPlugin } from "@heximon/http/compiler";
import { VercelFunctionsStrategy } from "@heximon/vercel";
export default defineHeximonConfig({
platform: new VercelFunctionsStrategy(),
plugins: [new HttpPlugin()],
});
VercelFunctionsStrategy (Vercel Functions + Edge Functions) and NetlifyFunctionsStrategy (Netlify Functions +
Edge Functions) are the hybrid targets. The placement is a build check against the resolved deploy target:
- a hybrid target honors both
runtimevalues; - an edge-only target (Cloudflare, Deno, Vercel Edge, Netlify Edge) already runs the whole app on edge —
runtime: "edge"is redundant and allowed,runtime: "node"is a build error (no node tier); - a node-only target (a plain Node server, AWS Lambda) has no edge tier —
runtime: "edge"is a build error.
What vp build emits
On vp build against a hybrid target, the compiler:
- Partitions the DI graph by runtime — it groups controllers by their
runtimeand computes each group's transitive provider closure (the providers each tier's controllers reach through their constructor signatures). A service injected by controllers on both tiers lands in both closures, instantiated per function (the two functions share no memory — a stateful shared service needs an external store). - Bundles each tier separately. Two bundler passes are mandatory: the edge function is self-contained
(everything inlined, edge resolve conditions), the node function keeps its dependencies external (resolved from
node_modules). A shared dependency can't be both, so the tiers bundle independently —dist/<output>.js(node) + a self-containeddist/edge.js. - Emits one deploy tree, routed by prefix. For Vercel, a single
.vercel/output/tree with both functions:
dist/.vercel/output/
config.json # routes: /feed → /edge, catch-all → /__server
functions/__server.func/index.js # the node bundle
functions/edge.func/index.js # the self-contained edge bundle (.vc-config: { runtime: "edge" })
"routes": [
{ "handle": "filesystem" },
{ "src": "/feed(/.*)?", "dest": "/edge" },
{ "src": "/(.*)", "dest": "/__server" }
]
/feed routes to the edge function; everything else falls through to the node catch-all. One
vercel deploy --prebuilt. Netlify is the same idea with a different shape: the node Functions catch-all plus a
prefix-routed Netlify Edge Function in .netlify/edge-functions/.
The guarantee: provably edge-safe before deploy
The edge tier bundles under an unenv-backed resolve policy (the unjs node:*
compatibility layer wrangler uses). Two effects:
- A polyfillable
node:builtin works on edge.node:crypto→globalThis.crypto,node:buffer, parts ofnode:stream— the bundle inlines the polyfill so the import bundles and runs, instead of being left external and breaking at runtime. More controllers can legitimately go edge. - A node-only builtin in an edge closure fails the build.
node:fs,node:net, a socket-level driver — the edge runtime can't run it, so an edge-closure import of one is a hard build error that names the offending module and the edge tier, attributed to the importing file. "theedgetier importsnode:fs, which this edge runtime cannot run (no working polyfill)" is a compile diagnostic, not a 3am crash.
(Netlify Edge runs on Deno, whose broad native node: compatibility means its edge tier keeps the simpler
node:-external policy.)
Dev, test, and the constraints
There is no in-process edge emulator, so vp dev and createTestApp run the whole app on Node — the edge
partition is a strict subset of what Node runs, so it is faithful. Author and test exactly as for any other app;
the split is a vp build concern.
Two v1 constraints:
- Edge controllers are authored inline, not contract-mode — a contract-mode controller carries its prefix in the contract value (which the compiler does not read), so it has no static prefix to route on.
- Cross-runtime calls use
ClientTransport.fetch(baseUrl), not the in-processInternalClient. On a split, the edge function's router carries only the edge tier's routes, so an in-process call to a node route can't reach it — the compiler rejects an edge closure that reaches theInternalClientseam, and steers you to an explicit fetch (a real HTTP request the platform routes to the other function).
See also
- Deploy overview —
vp buildand the deploy strategies this split builds on. - Controllers — the base
Controllerconfig (prefix,middlewares,errorFilters,scopes) thatruntimesits alongside. - Mixed-runtime split (Vercel)
— the
/feed(edge) +/posts(node) split end to end, with thevercel deploy --prebuiltoutput tree and a test proving the polyfillable-node:crypto-works / node-only-builtin-fails-the-build guarantee. - Mixed-runtime split (Netlify)
— the same split on
NetlifyFunctionsStrategy.
Nuxt
Run a Heximon API inside a Nuxt 5 app on one Nitro server, with SSR cookie forwarding, a typed Contract client with TanStack Vue Query bindings, heximon.config.ts, HttpPlugin, and a server/ controller.
Hosts
Where a Heximon app's fetch server lives — the heximon() Vite dev server, standalone Node/Bun/Deno, Nitro, Nuxt, unplugin for other bundlers, library mode, and the mixed-runtime split.