Deploying SignalX
Every platform runs the same app: a WinterCG createFetchHandler behind an
entry file you own. The Node server is built in; Cloudflare, Vercel and Netlify each have a
build adapter; Deno and Bun deploy from a few lines of config.
The deployment model
A SignalX SSR app deploys the same way everywhere:
- One runtime —
createFetchHandlerfrom@sigx/server-rendererturns aRequestinto aResponseon any WinterCG runtime. Bots get a complete blocking document; everyone else streams shell-first;useResponsestatus/redirects resolve before the first byte. - One build seam — an adapter passed to
sigx({ ssr: { adapter } })in your Vite config decides how the server bundle is built (externalized for Node and Bun, fully bundled for the edge) and what platform output is generated. - One entry you own —
src/entry.<platform>.tsis scaffolded on first build and never overwritten. It composes static assets → server functions → document render, and always exports the portable{ fetch }shape.
The platform matrix
| Platform | Adapter | serverBuild | Statics | Deploy with |
|---|---|---|---|---|
| Node | built-in nodeAdapter() (default) | external | express.static / node:http | node --conditions production server.mjs |
| Cloudflare Workers | @sigx/cloudflare | bundled, ['workerd','worker'] | wrangler assets — served before the worker runs | wrangler deploy |
| Deno / Deno Deploy | inline adapter object — no package | bundled, ['deno'] | serveDir (@std/http) fallthrough | deno deploy |
| Bun | reuses the Node build — no package | external | Bun.serve routes / Bun.file | bun --conditions=production server.bun.ts |
| Vercel — Node runtime | @sigx/vercel | bundled | Build Output static/ + filesystem route | vercel deploy --prebuilt |
| Vercel — Edge runtime | @sigx/vercel with { runtime: 'edge' } | bundled, ['edge-light','worker'] | Build Output static/ + filesystem route | vercel deploy --prebuilt |
| Netlify | @sigx/netlify | bundled | publish dir CDN, preferStatic | netlify deploy --prod |
The entry contract
Whatever the platform, the entry rhymes (Cloudflare shown):
// src/entry.cloudflare.ts — user-owned, ~20 lines
import { createFetchHandler } from '@sigx/server-renderer/server';
import { handleServerFnRequest, matchesServerFn } from '@sigx/server/server';
import { template, assets } from 'virtual:sigx-app';
import { serverFns } from 'virtual:sigx-server-fns';
import { createApp } from './entry-server';
const handler = createFetchHandler({
template,
app: (url) => createApp(url),
document: { assets }
});
export default {
async fetch(request: Request) {
if (matchesServerFn(request)) {
return handleServerFnRequest(request, {
resolve: (s) => serverFns[s]?.() ?? null
});
}
return handler(request);
}
};
The composition order is your routing policy: static assets first (served by the platform wherever possible), then the server-function endpoint, then the document render for everything else.
The static-tier rules
Two rules hold on every platform — the adapters encode them, and the hand-written entries (Deno, Bun) must keep them:
- GET/HEAD-only. POSTs belong to the server-function endpoint; a static tier
that answers other methods (e.g.
serveDir's 405) would swallow them. - Never serve the raw outlet template. The client build's
index.htmlis the unrendered document shell — every adapter keeps it off/(wranglerhtml_handling: "none", Vercel'sstatic/omits it, Netlify strips it from the publish dir), because the platform's static tier would otherwise shadow the document render.
Security defaults are unchanged by deployment — the server-function endpoint keeps its POST-only JSON, CSRF and origin gates on every platform; see Deployment & security.
Where to go next
- Node — the default, no adapter needed
- Deno & Deno Deploy / Bun — the package-less platforms
- Cloudflare, Vercel, Netlify — the adapter packages
- Deploy adapters in @sigx/vite — the
SigxAdaptercontract behind all of them
