# @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 ```bash bun add @wrnexus/dev-server ``` > Private package — the machine must be authenticated to the `wrnexus` npm org > (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported for the full server; the `node:http` adapter is for WinterCG embedding only). ## API ### Main entry (`@wrnexus/dev-server`) | Export | Kind | Purpose | | --------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `startServer(opts: ServeOptions)` | `Promise` | 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` | 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)` ```ts 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 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; // named dbs → getDb("") 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//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). ```ts interface RuntimeDeps { mode: Mode; hmr: boolean; // inject the live-reload client into pages router: Router; loadModule(file: string): Promise>; getMiddleware(): Promise; 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 + i18n?: ResolvedI18n; // enables ctx.t, , {t:key} markers inlineStyles?: string; // inline small prod stylesheets into assetVersion?: string; // cache-busting ?v= on framework asset URLs head?: string; // raw HTML appended to every page 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; 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. ```ts 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; 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). ```ts interface GatewayOptions { port?: number; // default 3000 hostname?: string; // default "localhost" 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; } ``` The gateway exposes `/__gateway/health` (JSON list of routed apps) and returns a `RunningGateway` (`{ port, url, stop() }`). ### `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. ```ts type FetchHandler = (req: Request) => Response | undefined | Promise; toRequest(req: IncomingMessage, opts?): Promise writeResponse(res: ServerResponse, response: Response): Promise // preserves multiple Set-Cookie nodeListener(handler: FetchHandler, opts?): (req, res) => Promise serveNode(handler: FetchHandler, opts?): Promise ``` ### Subpath export: `@wrnexus/dev-server/serve-entry` The child process the dev supervisor launches: ```bash bun run serve-entry.ts ``` 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 ```ts 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 ```ts 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` ```ts import { createProductionHandlers, serveNode } from "@wrnexus/dev-server"; const handlers = createProductionHandlers(manifest, opts); await serveNode(handlers.fetch, { port: 8080 }); ``` ### Multi-app gateway ```ts 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`, and `Bun.gzipSync`. The full app also relies on `bun:sqlite` / `Bun.SQL` via `@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). - `.wrn` files 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`.