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 { 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.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#