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

```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 <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.

`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).

```ts
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.

```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<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).

```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<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:

```bash
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

```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 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`.
</content>

</invoke>
