# @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. 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. ### `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. 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). ```ts 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. ```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`.