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:

  1. One runtimecreateFetchHandler from @sigx/server-renderer turns a Request into a Response on any WinterCG runtime. Bots get a complete blocking document; everyone else streams shell-first; useResponse status/redirects resolve before the first byte.
  2. 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.
  3. One entry you ownsrc/entry.<platform>.ts is 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#

PlatformAdapterserverBuildStaticsDeploy with
Nodebuilt-in nodeAdapter() (default)externalexpress.static / node:httpnode --conditions production server.mjs
Cloudflare Workers@sigx/cloudflarebundled, ['workerd','worker']wrangler assets — served before the worker runswrangler deploy
Deno / Deno Deployinline adapter object — no packagebundled, ['deno']serveDir (@std/http) fallthroughdeno deploy
Bunreuses the Node build — no packageexternalBun.serve routes / Bun.filebun --conditions=production server.bun.ts
Vercel — Node runtime@sigx/vercelbundledBuild Output static/ + filesystem routevercel deploy --prebuilt
Vercel — Edge runtime@sigx/vercel with { runtime: 'edge' }bundled, ['edge-light','worker']Build Output static/ + filesystem routevercel deploy --prebuilt
Netlify@sigx/netlifybundledpublish dir CDN, preferStaticnetlify deploy --prod

The entry contract#

Whatever the platform, the entry rhymes (Cloudflare shown):

TypeScript
// src/entry.cloudflare.ts — user-owned, ~20 lines
import { createFetchHandler } from '@sigx/server-renderer/server';
import { handleServerFnRequest, matchesServerFn } from '@sigx/server/server';
import { createBoundaryRefresh } from '@sigx/resume/server';
import { template, assets } from 'virtual:sigx-app';
import { serverFns, serverFnBase } from 'virtual:sigx-server-fns';
import { createApp } from './entry-server';

const handler = createFetchHandler({
    template,
    app: (url) => createApp(url),
    document: { assets }
});

const renderBoundaries = createBoundaryRefresh({ components: { /* … */ } });

export default {
    async fetch(request: Request) {
        if (matchesServerFn(request, serverFnBase)) {
            return handleServerFnRequest(request, {
                base: serverFnBase,
                resolve: (s) => serverFns[s]?.() ?? null,
                renderBoundaries
            });
        }
        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.

Two details in that entry are easy to leave out, and both fail silently:

  • serverFnBasevirtual:sigx-server-fns exports the mount path the build actually used, so the predicate and the handler cannot disagree with each other or with sigxServer({ base }). Everything after the base is the symbol, so a base that is wrong only in part slices the symbol at the wrong offset — worse than a clean miss. Passing it to both is the fix; the plugin warns at build time when a non-default base is configured and an entry still calls matchesServerFn(request) bare.
  • renderBoundaries — without it, single-flight boundary refresh never happens. Nothing errors; mutations simply stop updating server-rendered boundaries, and the client converges a round trip later through $cache. You do not need to import your server app here. Production builds inject a side-effect import of it at the top of virtual:sigx-server-fns, so an entry that imports the registry — as this one does — already has the pipeline stamped. Add an explicit import './server-app' only in an entry that does not touch the registry at all.

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.html is the unrendered document shell — every adapter keeps it off / (wrangler html_handling: "none", Vercel's static/ 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#

Running actors as well? A host is stateful and long-lived, so it has requirements this page does not cover — readiness that distinguishes draining from dead, a shutdown order that drains connections, and a backend for state. See Kubernetes and Cloudflare Workers.