@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 wire 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, wires 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; any other server change triggers process.exit(RESTART_EXIT_CODE) so the dev supervisor (@wrnexus/cli) respawns the process with fresh modules.
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 Wire 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. 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;
}
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 are compiled to TypeScript into a hidden sibling.wrnexus/cache dir and dynamically imported; the module cache means each edited server module needs a fresh process (dev) — hence the restart-on-change model.- 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.