Ships the apis { } block, the legacy/config cleanup, the wrnexus update
migrations, and the editor tooling that understands all of it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wrnexus/dev-server
The WrNexus HTTP + WebSocket server runtime — request dispatch, SSR document assembly, live-reload (HMR), and the portable production handler.
Part of the WrNexus framework — an SSR-first, Bun-native full-stack web framework.
Overview
This package is the server runtime that powers a WrNexus app in both development and production. A single request runtime (createHandlers) owns HTTP/WebSocket dispatch and SSR document assembly; it knows nothing about how modules and assets are produced, so the dev and prod entry points wrn in different backends: dev uses dynamic module loading plus on-the-fly bundling and injects a live-reload client; prod uses a static, pre-built manifest with cache-immutable assets. The package also ships a multi-app gateway (route several apps by Host header behind one port) and a portable node:http adapter for WinterCG hosts. It is entirely server-side and Bun-native (Bun.serve, Bun.file, Bun.gzipSync).
Installation
bun add @wrnexus/dev-server
Private package — the machine must be authenticated to the
wrnexusnpm org (a read token in~/.npmrc). Requires Bun (Node is not supported for the full server; thenode:httpadapter is for WinterCG embedding only).
API
Main entry (@wrnexus/dev-server)
| Export | Kind | Purpose |
|---|---|---|
startServer(opts: ServeOptions) |
Promise<RunningServer> |
Start the dev server on Bun.serve: builds the router, connects/migrates databases, connects assets + HMR, and starts the file watcher. |
createHandlers(deps: RuntimeDeps) |
Handlers |
The shared request runtime (fetch + websocket handlers). Re-exported from runtime.ts. |
createProductionServer(manifest, opts) |
Bun.Server |
Start the production server from a precompiled manifest. |
createProductionHandlers(manifest, opts) |
Handlers |
Build the portable prod fetch/websocket handlers with no server bound (the deployment-adapter seam). |
startGateway(opts: GatewayOptions) |
Promise<RunningGateway> |
Boot multiple apps as child processes and route by Host. |
toRequest, writeResponse, nodeListener, serveNode |
functions | node:http ↔ WinterCG Request/Response adapter. |
RESTART_EXIT_CODE |
number (97) |
Exit code the dev child uses to ask the supervisor for a fresh process. |
STYLES_HREF, HMR_CLIENT_JS |
constants | The global stylesheet URL and the inline HMR client script. |
Exported types: ServeOptions, RunningServer, RuntimeDeps, AssetServer, WsData, GatewayApp, GatewayOptions, GatewayAuth, GatewaySecurity, RunningGateway, FetchHandler.
startServer(opts)
interface ServeOptions {
appDir: string; // absolute/relative path to the app/ dir
port?: number; // default 3000
hostname?: string; // default "localhost"
mode?: Mode; // "development" | "production"; default "development"
hmr?: boolean; // inject live-reload client; default (mode === "development")
styleEntry?: string | null; // resolved absolute path to the global CSS entry
stylesConfig?: StylesConfig; // custom styles processor (e.g. Tailwind/PostCSS)
head?: string; // raw HTML appended to every page <head>
seo?: SeoConfig; // global SEO defaults
security?: SecurityConfig; // security headers + CORS policy
theme?: ThemeConfig; // design-token theme (merged over built-in light/dark)
i18n?: I18nConfig; // default language + supported locales
db?: { driver: string; url: string }; // default db → getDb(); dev auto-migrates
databases?: Record<string, { driver: string; url: string }>; // named dbs → getDb("<name>")
realtime?: { scale?: boolean; redisUrl?: string }; // bridge rooms over Redis across processes
}
interface RunningServer {
port: number;
hostname: string;
url: string;
router: Router;
stop(): void;
}
In development, startServer also connects app/db/migrations (and app/db/<name>/migrations) and auto-applies migrations, then starts an in-process file watcher. CSS edits hot-swap live. Page, component, layout, API, middleware, realtime, schema, locale, and public-asset edits invalidate only their cached modules, rescan routes where necessary, and morph fresh HTML through the existing HMR WebSocket. The server process and active gateway stay running.
Custom not-found handlers
Add app/pages/404.wrn to customize unmatched frontend routes. The rendered
page keeps the requested response's HTTP 404 status. Add app/api/404.ts
with normal HTTP method exports to customize unmatched backend/API responses;
its response body and headers are preserved and its status is normalized to
404.
getWrnCompileMetrics() exposes cumulative content-addressed compiler cache
hits, misses, successful compilations, errors, totalDurationMs, and
lastDurationMs for the DevToolbar or custom diagnostics. Tests and embedded
servers can call resetWrnCompileMetrics() to establish a fresh measurement
window.
createHandlers(deps)
The core runtime shared by dev and prod. It handles CORS preflight, /healthz and /__wrnexus/health, request-body size limits (413), HMR socket upgrades (/__wrnexus/hmr), realtime WebSocket upgrades (defineRoom default export or a raw websocket export), the middleware pipeline, API routes (/api/*), framework assets (/__wrnexus/*), public assets, and full SSR page rendering (component mounts, layouts, slots, i18n markers, per-page script selection, ETag/304, gzip).
interface RuntimeDeps {
mode: Mode;
hmr: boolean; // inject the live-reload client into pages
router: Router;
loadModule(file: string): Promise<Record<string, unknown>>;
getMiddleware(): Promise<Middleware[]>;
assets: AssetServer; // serves /__wrnexus/* (islands, reactive, hmr)
hasStyles?: boolean; // inject the global stylesheet link
hasUi?: boolean; // inject the WrNexus UI stylesheet (/__wrnexus/ui.css)
theme?: ResolvedTheme; // enables /__wrnexus/theme.css + <html data-theme>
i18n?: ResolvedI18n; // enables ctx.t, <html lang>, {t:key} markers
inlineStyles?: string; // inline small prod stylesheets into <head>
assetVersion?: string; // cache-busting ?v= on framework asset URLs
head?: string; // raw HTML appended to every page <head>
seo?: SeoConfig;
security?: SecurityConfig;
maxBodyBytes?: number; // 413 above this; default 10 MB
hub?: HmrHub; // browser HMR sockets (dev only)
realtimeBus?: RealtimeBus; // cross-process room bridge (Redis pub/sub)
}
interface Handlers {
fetch(req: Request, server: UpgradeServer): Promise<Response | undefined>;
websocket: { open; message; close; drain };
}
WsData is the per-connection socket tag — a discriminated union of { kind: "realtime"; handler }, { kind: "room"; meta }, or { kind: "hmr" }.
createProductionServer(manifest, opts) / createProductionHandlers(manifest, opts)
Production runs the same request runtime as dev, but with no filesystem scan and no runtime bundling. wrnexus build emits an entry that statically imports every route/component/layout module and passes them as a ProdManifest; the route-matching tables are rebuilt from the raw patterns.
interface ProdManifest {
pages: { raw: string; mod: RouteModule }[];
api: { raw: string; mod: RouteModule }[];
realtime: { raw: string; mod: RouteModule }[];
middleware: Middleware[];
components: { name: string; mod: RouteModule }[];
layouts: { name: string; mod: RouteModule }[];
}
interface ProdOptions {
stylesPath?: string;
inlineStyles?: string;
reactivePath?: string;
themePath?: string;
themeJsPath?: string;
theme?: ResolvedTheme;
uiCssPath?: string;
schemasJs?: string;
i18n?: ResolvedI18n;
db?: { driver: string; url: string };
databases?: Record<string, { driver: string; url: string }>;
realtime?: { scale?: boolean; redisUrl?: string };
assetVersion?: string;
publicDir?: string;
head?: string;
seo?: SeoConfig;
security?: SecurityConfig;
port?: number;
hostname?: string;
maxBodyBytes?: number;
}
createProductionServer also loads the .env cascade for the production profile, installs SIGTERM/SIGINT graceful shutdown, and binds 0.0.0.0 (port from opts.port or $PORT, default 3000). Migrations are not run here — apply them first (wrnexus db migrate). createProductionHandlers returns the bare handlers for edge/serverless/node:http deployment.
startGateway(opts) — multi-app gateway
Serves several apps behind one port and routes each request to the right app by its Host header. Each app runs as its own child process (full isolation); the gateway is a thin host-based reverse proxy for HTTP and WebSocket. In development, normal application edits are applied inside the existing child and sent through its existing HMR connection. The child supervisor remains as crash recovery rather than the normal update path. Apps communicate at runtime via @wrnexus/pubsub (use the Redis driver so messages cross processes).
interface GatewayOptions {
port?: number; // default 3000
hostname?: string; // dev: "127.0.0.1"; production: "0.0.0.0"
mode?: "development" | "production";
apps: GatewayApp[];
security?: GatewaySecurity;
}
interface GatewayApp {
name: string; // app id (for logs)
dir: string; // app root (contains app/ + wrnexus.config.ts)
domains: string[]; // host names routed here
port?: number; // fixed internal port; else assigned
auth?: GatewayAuth; // per-app edge access control
}
interface GatewayAuth {
basic?: { user: string; pass: string } | Array<{ user: string; pass: string }>;
allowIps?: string[]; // exact-match IP allowlist
forward?: { url: string }; // forward-auth (SSO): 2xx allows
}
interface GatewaySecurity {
trustedHostsOnly?: boolean; // 404 unknown hosts instead of first app
rateLimit?: { max: number; windowMs?: number }; // global by client IP (429)
headers?: boolean; // add baseline edge security headers
forwardedHeaders?: boolean; // set X-Forwarded-* (default true)
accessLog?: boolean;
}
Forward auth is a verification hook, not a login page. Configure forward.url with a
dedicated endpoint such as http://sso.localhost:3000/api/verify. The gateway forwards
the request's Cookie and Authorization headers plus X-Forwarded-Host,
X-Forwarded-Proto, X-Original-Method, and X-Original-Uri (including its query
string). The verifier must return 2xx only for an authenticated session and 401/403
otherwise. Pointing forward auth at an SSO home page that always returns 200 allows
every request and does not implement SSO.
For browser SSO, the verifier may return a 302/303/307/308 with a Location
header pointing to its login page. The gateway passes that redirect to the browser. The
login flow should validate a signed returnTo value before redirecting back; API clients
should receive 401/403 instead of an HTML login redirect.
Open the gateway URL (normally http://127.0.0.1:3000), not an app's internal
port. The gateway exposes /__gateway/health (JSON list of routed apps) and returns a
RunningGateway ({ port, url, stop() }). Use --host=0.0.0.0 when other devices need
to reach a development gateway.
node:http adapter (from ./adapters/node.ts)
For embedding the WinterCG handler behind an existing Node server or a WinterCG host. Note the full app still needs Bun-compatible globals (Bun.file, bun:sqlite, etc.); only the Request/Response conversion is fully portable.
type FetchHandler = (req: Request) => Response | undefined | Promise<Response | undefined>;
toRequest(req: IncomingMessage, opts?): Promise<Request>
writeResponse(res: ServerResponse, response: Response): Promise<void> // preserves multiple Set-Cookie
nodeListener(handler: FetchHandler, opts?): (req, res) => Promise<void>
serveNode(handler: FetchHandler, opts?): Promise<Server>
Subpath export: @wrnexus/dev-server/serve-entry
The child process the dev supervisor launches:
bun run serve-entry.ts <appDir> <port> <mode>
It loads the optional wrnexus.config.ts, resolves the style entry, calls startServer, and prints the route table (Pages / API / Realtime / Components). Because it runs in its own process, every restart re-imports all route modules fresh — that is how the supervisor delivers live reload of edited server code. startGateway resolves this entry via import.meta.resolve("@wrnexus/dev-server/serve-entry") to spawn each dev app.
Usage
Programmatic dev server
import { startServer } from "@wrnexus/dev-server";
const server = await startServer({
appDir: "./app",
port: 3000,
mode: "development",
theme: {/* design tokens */},
db: { driver: "sqlite", url: "file:./data/app.db" },
});
console.log(`Running at ${server.url}`);
// server.stop();
Production server from a build manifest
import { createProductionServer } from "@wrnexus/dev-server";
import { manifest } from "./dist/manifest.js"; // generated by `wrnexus build`
createProductionServer(manifest, {
stylesPath: "./dist/styles.css",
reactivePath: "./dist/reactive.js",
assetVersion: process.env.BUILD_ID,
db: { driver: "postgres", url: process.env.DATABASE_URL! },
port: Number(process.env.PORT) || 3000,
});
Embedding the handler on node:http
import { createProductionHandlers, serveNode } from "@wrnexus/dev-server";
const handlers = createProductionHandlers(manifest, opts);
await serveNode(handlers.fetch, { port: 8080 });
Multi-app gateway
import { startGateway } from "@wrnexus/dev-server";
await startGateway({
port: 3000,
apps: [
{ name: "web", dir: "./apps/web", domains: ["localhost", "web.localhost"] },
{
name: "admin",
dir: "./apps/admin",
domains: ["admin.localhost"],
auth: { basic: { user: "root", pass: "s3cret" } },
},
],
security: { trustedHostsOnly: true, rateLimit: { max: 600 } },
});
Framework asset routes
The runtime serves these framework-owned paths (dev builds them live; prod serves pre-built/immutable versions):
/__wrnexus/nav.js,/__wrnexus/reactive.js,/__wrnexus/realtime.js— client runtimes/__wrnexus/validate.js,/__wrnexus/schemas.js,/__wrnexus/i18n.js— validation + i18n runtimes/__wrnexus/theme.css,/__wrnexus/theme.js,/__wrnexus/ui.css,/__wrnexus/styles.css— styles/__wrnexus/hmr— dev-only HMR WebSocket/__wrnexus/csr— server-evaluated CSR bindings for browser-side API fetches
Pages get only the scripts they use: nav.js always, reactive.js when a page has a data-scope/CSR fetch, plus theme/validation/i18n/realtime runtimes when the relevant markup is present.
Requirements / Notes
- Bun-only. Uses
Bun.serve(HTTP + WebSocket),Bun.file, andBun.gzipSync. The full app also relies onbun:sqlite/Bun.SQLvia@wrnexus/db. - Orchestrates the whole framework:
@wrnexus/core(context, security, realtime registry),@wrnexus/router,@wrnexus/ssr(renderDocument),@wrnexus/csr(client runtimes),@wrnexus/compiler(.wrn→ TS),@wrnexus/styles,@wrnexus/ui,@wrnexus/validation,@wrnexus/i18n,@wrnexus/db, and@wrnexus/pubsub(Redis-backed cross-process realtime). .wrnfiles compile into a content-addressed hidden.wrnexus/cache. Targeted invalidation gives changed modules a fresh import identity without restarting the development server.- Responses are gzipped when the client accepts it and the body is a buffered, compressible payload ≥ 1 KB; streaming/SSE responses opt out via
Cache-Control: no-transform.