# WrNexus — The Complete Guide

> One document to build WrNexus apps fast and correctly. It covers **every package**, the
> **whole `.wrn` language**, **all attributes and config properties**, and a **step‑by‑step**
> path from `create` to a full app: pages, components, reactivity, APIs, validation, database,
> auth, i18n, theming, realtime, testing, and deploy.

WrNexus is an **SSR‑first**, **Bun‑first** full‑stack framework. Pages render to HTML on the
server; interactivity is added by **server‑rendered components** hydrated in the browser by a
single generic runtime (no per‑component bundles). The `.wrn` language compiles to TypeScript.

**Conventions used here**

- `app/…` paths are relative to your app root (where `wrnexus.config.ts` lives).
- ⚠️ marks a gotcha that bites people.
- Reactive/runtime attributes owned by WrNexus are called **directives** (`data-for`, `@event`, …).

---

## Table of contents

**Part I — Getting started**

1. Install & create · 2. Run · 3. Project structure · 4. The request lifecycle

**Part II — The `.wrn` language (complete)**

- 5. `page` vs `component` · 6. Every block keyword · 7. The `view`: interpolation, events, directives · 8. Slots · 9. Gotchas

**Part III — Build an app, feature by feature**

- 10. A page · 11. Layouts · 12. Components & props · 13. Reactivity · 14. WrNexus UI · 15. Theming · 16. Styles · 17. API routes · 18. Validation (forms + API) · 19. Database · 20. Auth, sessions & CSRF · 21. Middleware · 22. i18n · 23. SEO · 24. Security headers & CORS · 25. Realtime · 26. SSR/CSR data bindings · 27. Optional packages · 28. Config profiles & env · 29. Testing · 30. Build & deploy

**Part IV — Reference**

- 31. CLI · 32. `wrnexus.config.ts` schema · 33. Directive & attribute cheat sheet · 34. Package API index · 35. The `Context` object · 36. Editor support

---

---

# Part I — Getting started

## 1. Install & create

Requires [Bun](https://bun.sh) ≥ 1.1.

```bash
# From the WrNexus repo, the CLI is packages/cli. In a published setup:
wrnexus create my-app
cd my-app
bun install
```

`wrnexus create <name>` scaffolds: `package.json`, ESLint/Prettier config, `wrnexus.config.ts`
(minimal), `public/robots.txt`, and starter `app/pages/index.tsx`, `app/pages/about.tsx`,
`app/api/hello.ts`, `app/middleware/logger.ts`, `app/realtime/chat.ts`, `app/client/counter.ts`.
It refuses to overwrite an existing directory.

## 2. Run

```bash
wrnexus dev .                 # dev server, live reload/HMR, auto-migrates DB, regenerates typed routes+queries
wrnexus dev . --port=4000     # pick a port (default 3000)
wrnexus build .               # production bundle → dist/server.js  (production profile auto-applied)
bun dist/server.js           # run the built server (honors PORT env)
```

`wrnexus dev` is a supervisor: it regenerates typed DB queries and typed routes, then spawns the
dev server child (which owns file watching + HMR — CSS/component edits stream over WebSocket with
no full reload). On a crash it respawns.

## 3. Project structure

```
my-app/
  wrnexus.config.ts       # optional: seo, head, security, theme, i18n, db, styles, profiles, port
  public/                # static assets → served at / (robots.txt, images, …)
  app/
    pages/               # file-based pages   → routes  (index.wrn → /, about.wrn → /about, users/[id].wrn → /users/:id)
    components/          # reusable .wrn components (mount with data-component="name")
    layouts/             # layout .wrn files (a component with a <slot>); page picks via layout = "name"
    api/                 # file-based API routes → /api/*  (export GET/POST/… handlers)
    middleware/          # global middleware (one file = one middleware; run in the chain)
    realtime/            # WebSocket rooms → /realtime/*  (export default defineRoom({...}))
    schemas/             # validation schemas (v.object(...)) shared by forms + APIs
    styles/              # global CSS (global.css → every page)
    locales/             # i18n dictionaries <lang>.json (opt-in i18n)
    db/
      schema.ts          # TS models — the source of truth
      migrations/        # *.sql (-- +up / -- +down)
      queries/           # *.sql (typed query definitions)
      queries.gen.ts     # AUTO-GENERATED from queries/*.sql
      seed.ts            # re-runnable dev seed data
    routes.gen.ts        # AUTO-GENERATED typed route table
```

The app folder is convention‑based: drop a file in the right directory and it becomes a route.
Requests are matched against a table scanned at startup — user input never becomes a file path.

## 4. The request lifecycle

1. Middleware chain (`app/middleware/*`) runs in order; any middleware may short‑circuit.
2. Router matches the path → page, API route, or realtime room.
3. **Pages**: the page renders HTML → components (`data-component`) are rendered on the server and
   spliced in → the layout wraps it → SEO `<head>` is built → the reactive runtime is injected
   **only if** the page contains reactive markup.
4. **API routes**: your `GET`/`POST`/… handler returns a `Response`.
5. Security headers are applied; the response is sent.

---

---

# Part II — The `.wrn` language (complete)

## 5. `page` vs `component`

A `.wrn` file is **exactly one** top‑level block:

```
page Home { … }        // a route (file path under app/pages/ → URL)
component Counter { … } // a reusable, prop-driven fragment (mounted via data-component)
```

Both parse to the same shape but compile differently:

|                                       | `page`                                                      | `component`                                                             |
| ------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
| Becomes a route                       | ✅ (from file path)                                         | ❌ (mounted in a page)                                                  |
| `props`                               | ignored                                                     | ✅ coerced + injected                                                   |
| `layout`                              | ✅ selects a layout                                         | ignored                                                                 |
| `api` / `ssr` / `client` / `realtime` | ✅ emitted                                                  | ignored                                                                 |
| Non‑state `{expr}` in view            | live **client mustache**                                    | **baked server‑side** (HTML‑escaped)                                    |
| Ships JS?                             | only if it has `state`, `@event`, `{expr}`, or CSR bindings | only if it has `state`/`@event` (pure prop components ship **zero JS**) |

**Layouts are components.** A layout file is a `component` with a `<slot>` where the page body goes.
A page selects one with `layout = "public"` (→ `app/layouts/public.wrn`); `layout = "none"` opts out.

## 6. Every block keyword

Inside `page`/`component { … }`, these members are allowed (zero or more of each unless noted):

### `layout` (page only)

```
layout = "public"
```

### `props { }` (component) — ⚠️ one declaration per line

```
props {
  title: string               // required: no default
  start: number = 0           // start="5" arrives as number 5
  disabled: boolean = false
  items: Array<Item> = []
}
```

Type annotations use TypeScript syntax. Legacy declarations without an annotation still infer their
runtime type from the default value.

### `types { }` — local TypeScript models

```ts
types {
  interface Item { id: string; label: string }
  type Status = "idle" | "loading" | "ready"
}
```

These declarations can be referenced by props, state, and function signatures in the same file.

### `state` — ⚠️ one per line

```
state count: number = start        // may reference a prop or earlier state
state status: Status = "idle"
```

State seeds the reactive scope. On pages, seeds are also evaluated at compile time for SSR baking.

### `view { }`

```
view { <html> … </html> }
```

Plain HTML with interpolation, events, and directives — see §7.

### `seo { }` (page)

```
seo {
  title = "Home"
  description = "Welcome"
  canonical = "/hello"
}
```

Keys are arbitrary and land in the page `meta` (merged with global `seo` config). Values may be
quoted or bare‑to‑end‑of‑line.

### `style { }` — scoped CSS, inlined with the page/component

```
style {
  .box { background: var(--wrn-color-surface); border-radius: var(--wrn-radius); }
}
```

Multiple `style` blocks accumulate. Use theme tokens (`var(--wrn-*)`) so styles restyle on theme
change.

### `functions { }` — typed helpers

```
functions {
  function slug(value: string): string {
    return value.toLowerCase().replace(/\s+/g, "-");
  }
}
```

Component functions are available to events, lifecycle hooks, and watchers in the browser as well as
server rendering. Page helpers are available to `api`, `ssr`/`client`, and `realtime` bodies.

### `api <METHOD> <path> { }` — colocated API route

```
api POST /subscribe {
  const body = await ctx.req.json();
  return Response.json({ ok: true });
}
```

Method is upper‑cased; path is auto‑prefixed to `/api` and traversal‑checked. `ctx` is in scope.

### `ssr { }` / `client { }` — named data bindings (see §26)

```
ssr {
  functions { function names(u){ return u.map(x=>x.name).join(", "); } }
  api userList GET /api/users/ssr {   // ⚠️ note the NAME before the method
    return names(users);
  }
}
```

Each may contain only `api <name> <METHOD> <path> { }` and `functions { }`. Reference a binding
from the view with `api="userList"` on an element.

### `realtime <name> { on <evt>(args) { } }` — inline WS handlers (legacy form)

```
realtime chat {
  on message(data) { broadcast(data); }
}
```

Compiles to `export const websocket = { message(ws, data) { … } }`. For real apps prefer a
standalone `app/realtime/<name>.ts` with `defineRoom` (§25); the inline form is the compiler's
original shape.

## 7. The `view`: interpolation, events, directives

The `view` body is a lenient HTML parser. Elements, self‑closing `<tag/>`, void elements
(`<br>`, `<img>`, …), and `<!-- comments -->` (dropped) all work. **Attribute values must be
quoted.** A valueless attribute is boolean (`<button disabled>`).

### Text interpolation `{expr}`

- **Page + references `state`** → baked to `<span data-text="expr">value</span>`; no‑JS clients see
  the real value, the runtime keeps it live.
- **Page + non‑state** → left as a literal mustache; the runtime fills it at hydration.
- **Component + prop/constant** → baked server‑side (HTML‑escaped), **not** reactive.
- **Component + state** → reactive `data-text` span.

Both `{expr}` and `{{ expr }}` are accepted.

⚠️ **Attribute interpolation** (`href="{url}"`) is compiled **only in components**. On a **page**,
`{expr}` inside a normal attribute is inert (except inside a `data-for` item). Prefer components for
attribute interpolation.

### Translations `{t:key}`

```
<h1>{t:home.title}</h1>
<div data-component="counter" label="{t:nav.dashboard}"></div>   <!-- works in prop values too -->
```

Compiles to `<span data-t="key">` and is resolved per request against the active language. It also
resolves inside component prop values.

### Event bindings `@event="statement"`

```
<button @click="count++">+</button>
<button @click="count = 0">reset</button>
<input @input="name = 'x'">
```

`@name` compiles to `data-on-name`. The event name is arbitrary (any DOM event, hyphens allowed).
The statement runs in the element's reactive scope. Supported statements: `x++`/`x--`,
`x = expr`, compound assign (`+= -= *= /= %=`), bare expressions/method calls, and multiple
statements separated by `;`. Expressions use a **CSP‑safe, eval‑free** parser: literals,
identifiers, member/index access, calls, arrays/objects, `+ - * / %`, comparisons, `== != === !==`,
`&& ||`, unary `! - +`, and ternary `?:`.

### Directives (the `data-*` the runtime understands)

| Directive         | Syntax                                                                       | What it does                                                                                                                                                                                                     |
| ----------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data-scope`      | `data-scope="count: 0, name: 'x'"`                                           | Declares reactive state on a subtree. The compiler emits this automatically when a page/component has state. A binding is owned by its **nearest** `data-scope` ancestor (nesting is safe).                      |
| `data-on-<event>` | `data-on-click="count++"`                                                    | Event handler (the compiled form of `@event`).                                                                                                                                                                   |
| `data-text`       | `data-text="count * 2"`                                                      | `textContent` follows the expression. Emitted by state interpolation; you can also hand‑write it.                                                                                                                |
| `data-show`       | `data-show="open"`                                                           | Toggles visibility while preserving interactive state. **Hand-authored** (no `{}` sugar).                                                                                                                        |
| `data-for`        | `data-for="item in items"`, optionally `key item.id` or `data-key="item.id"` | Repeats the element per list item. A stable key preserves DOM identity during reorder; unkeyed loops retain legacy full rerendering. Inside, item/index locals work in bindings and handlers. **Hand-authored.** |
| `data-component`  | `data-component="counter"`                                                   | Mounts a component (server‑rendered, then hydrated). See §12.                                                                                                                                                    |
| `data-slot`       | `<div data-slot="header">…</div>`                                            | Fills a named `<slot name="header">` of a component (see §8).                                                                                                                                                    |

`{#if}` and `<Component is={...}>` keep inactive elements out of the live DOM. `data-show` keeps
stateful controls mounted and only changes visibility. Client-side conditions are not
authorization: anything delivered to the browser can be recovered by the user. Enforce permissions
in server loaders and API handlers, and never send an unauthorized branch's sensitive data.

Example — a reactive list you write by hand:

```
view {
  <div data-scope="items: [{t:'a'},{t:'b'}], open: true">
    <button @click="open = !open">toggle</button>
    <ul data-show="open">
      <li data-for="i in items key i.id" data-text="i.t"></li>
    </ul>
  </div>
}
```

There are also framework attributes consumed by **other** runtimes (loaded only when present):
`data-wrn-theme-toggle`/`data-wrn-theme-set` (theme, §15), `data-schema`/`data-error`/
`data-success`/`data-redirect` (forms, §18), `data-wrn-lang-set`/`data-wrn-lang` (i18n, §22),
`data-room*` (realtime, §25).

Cross-platform pages can use `data-native-browser="capability"` and
`data-native-mobile="capability"` with JSON `data-native-options`. Use
`data-native-only="browser|mobile|ios|android"` for platform visibility,
`data-native-requires="capability"` for support gating, and `@browser-event` /
`@mobile-event` for platform-specific reactive handlers. These declarative capability
actions run in browsers and Capacitor WebViews. Expo compilation selects mobile markup
and handlers but requires installed Expo APIs to be called from native screen code.

## 8. Slots

A component's output may include slots; the mount's children fill them.

```
// app/layouts/dashboard.wrn
component Dashboard {
  view {
    <header><slot name="actions"></slot></header>
    <main><slot></slot></main>          <!-- default slot: the page body -->
  }
}
```

```
// a page using it
page Reports {
  layout = "dashboard"
  view {
    <div data-slot="actions"><button>New</button></div>   <!-- fills the named slot; wrapper is dropped -->
    <h1>Reports</h1>                                       <!-- non-slotted content → default slot -->
  }
}
```

`<slot>fallback</slot>` keeps its fallback when nothing is provided.

## 9. Gotchas

- **`props`/`state` are one‑per‑line** (the value is read to end‑of‑line). No commas/semicolons.
- **Comments:** `//` outside `view`; `<!-- … -->` inside `view` (a `//` in `view` is literal text).
- **`{`/`}` are reserved** in view text for interpolation.
- **Void elements** take no closing tag.
- **`ssr`/`client` `api` has a name before the method**; top‑level `api` does not.
- **`props` on a `page` are ignored** — they only matter for `component`.
- **Reserved JS words** as prop/state names are fine (auto‑renamed internally in components).

---

---

# Part III — Build an app, feature by feature

Each step is self‑contained. Do them in any order you need.

## 10. A page

`app/pages/index.wrn` → `/`. A trailing `index` segment is dropped; `[param]` makes a dynamic
segment (`app/pages/users/[id].wrn` → `/users/:id`, `ctx.params.id`).

```
page Home {
  layout = "public"
  seo { title = "Home"  description = "Welcome to my app." }
  view {
    <h1>Hello WrNexus</h1>
    <p><a href="/about">About</a></p>
  }
}
```

Scaffold: `wrnexus generate page home` (alias `g p`).

## 11. Layouts

`app/layouts/public.wrn`:

```
layout Public {
  view {
    <nav><a href="/">Home</a> · <a href="/about">About</a></nav>
    <main><slot></slot></main>
  }
}
```

A page opts in with `layout = "public"`. Missing → a `default` layout if present; `layout = "none"`
disables layouts for that page.

`app/layouts/document.wrn` is the optional global outer layout. It wraps the
selected page layout and can define `<html>`, `<head>`, `<body>`, and `#app`.
It receives `cookies`, `theme`, `language`, `url`, and `pathname` during SSR.
Framework metadata, configured head content, styles, and scripts are merged into
the authored document automatically.

## 12. Components & props

`app/components/counter.wrn`:

```
component Counter {
  props {
    start = 0
    label = "Count"
  }
  state count = start
  view {
    <button @click="count++">{label}: {count}</button>
  }
}
```

Mount it in any page/view — pass props as attributes:

```
<div data-component="counter" start="5" label="Clicks"></div>
```

**Prop coercion:** each prop is coerced to the **type of its default** — number default → `Number(v)`
(so `start="5"` → `5`), boolean default → truthy check (`""`/`"true"`/`true` → true), else `String`.
Missing attribute → the default. Components can mount components (up to depth 15). Scaffold:
`wrnexus generate component counter`.

## 13. Reactivity

Everything is driven by `state` + directives (§7). Cheat sheet:

```
component Demo {
  state count = 0
  state open = true
  state items = ["a", "b", "c"]
  view {
    <p>Count doubled: {count * 2}</p>              <!-- reactive data-text -->
    <button @click="count++">+</button>
    <button @click="count = 0">reset</button>

    <button @click="open = !open">toggle</button>
    <ul data-show="open">
      <li data-for="x in items" data-text="x"></li>
    </ul>
  }
}
```

The runtime tracks dependencies automatically and re‑renders only what changed. No `new Function`,
no `eval` — CSP‑safe.

## 14. WrNexus UI

`@wrnexus/ui` ships **24** themed components, auto‑discovered like your own. Mount with
`data-component`; every component accepts a `class` prop (appended to the root) and is styled via
`--wrn-*` tokens + `.wrn-*` classes you can override.

**Layout:** `container`, `stack` (`gap`), `hstack` (`gap`, `align`), `grid` (`cols`, `gap`),
`spacer`, `divider`, `card`.
**Form/controls:** `button` (`label`, `variant`, `size`, `type`), `input` (`type`, `name`, `value`,
`placeholder`), `textarea` (`name`, `placeholder`, `rows`), `select` (`name`; options in slot),
`checkbox` (`name`, `label`), `radio` (`name`, `value`, `label`), `switch` (`name`, `label`).
**Display/feedback:** `badge` (`label`, `variant`), `tag` (`label`, `variant`), `alert`
(`variant`, `title`, `message`), `avatar` (`src`, `alt`), `spinner`, `progress` (`value`, `max`),
`skeleton` (`width`, `height`), `tooltip` (`text`; trigger in slot), `disclosure` (`summary`),
`table` (thead/tbody in slot), `theme-toggle` (`label`).

Variants: buttons `default|primary|danger|ghost`, sizes `sm|md|lg`; badge/tag/alert
`default|primary|success|danger|warning` (alert uses `info|success|danger|warning`).

```
<div data-component="button" variant="primary" label="Save"></div>
<div data-component="alert" variant="success" title="Done" message="Saved."></div>
<div data-component="grid" cols="3" gap="4">…</div>
```

**Override styling**, in priority order: (1) theme tokens `--wrn-*`, (2) redefine `.wrn-*` in your
CSS (loads after `ui.css`), (3) the `class` prop, (4) `wrnexus eject <name>` to copy the component's
`.wrn` source into `app/components/` (your copy shadows the library one).

## 15. Theming

Themes are flat token maps rendered as `--wrn-<key>` CSS variables (server + client). Built‑in
`light` and `dark`; the cookie `wrn-theme` selects one.

```ts
// wrnexus.config.ts
theme: {
  palette: "violet",              // blue | indigo | violet | emerald | cyan | rose | amber | slate
  default: "dark",                 // used when no cookie
  themes: {
    light: { "color-primary": "#2563eb" },   // deep-merged over built-in light
    dark:  { "color-primary": "#6c8cff" },
  },
}
```

Built‑in token keys include: `color-scheme`, `color-bg`, `color-surface`, `color-surface-2`,
`color-text`, `color-muted`, `color-border`, `color-primary`, `color-primary-hover`,
`color-primary-contrast`, `color-danger`, `color-success`, `color-warning`, `radius`, `radius-sm`,
`font-sans`, `shadow-1`. (The reserved key `color-scheme` emits the native property.)

Toggle from the view with zero JS:

```
<button data-wrn-theme-toggle>Toggle theme</button>
<button data-wrn-theme-set="light">Light</button>
```

The client exposes `window.wrnTheme = { get, set, toggle, bind, themes }`.

## 16. Styles

Global CSS entry defaults to `app/styles/global.css` (or all `app/styles/*.css` aggregated). The
built‑in Bun bundler resolves `@import` (including node_modules) and minifies in production.

Add a CSS framework two ways:

- **CDN** (zero build): `head: ['<link rel="stylesheet" href="https://cdn…/bootstrap.min.css">']`.
- **Custom processor** (Tailwind/PostCSS/Sass):

```ts
styles: {
  entry: "app/styles/global.css",
  process: async ({ entryPath, appRoot, mode }) => {
    const args = ["@tailwindcss/cli", "-i", entryPath!];
    if (mode === "production") args.push("--minify");
    return await Bun.$.cwd(appRoot)`bunx ${args}`.text();
  },
}
```

## 17. API routes

`app/api/<name>.ts` → `/api/<name>`. Export one function per HTTP method. Nested folders and
`[param]` segments work like pages.

```ts
// app/api/echo.ts   →  /api/echo
import type { Context } from "@wrnexus/core";

export const GET = async () => Response.json({ usage: "POST JSON here" });

export const POST = async (ctx: Context) => {
  const body = await ctx.req.json();
  return Response.json({ received: body });
};
```

`ctx` gives you `ctx.req`, `ctx.url`, `ctx.params`, `ctx.session`, `ctx.user`, `ctx.t`, `ctx.lang`,
`ctx.locals`, etc. (§35). Scaffold: `wrnexus generate api echo`.

You can also colocate an API in a page with the `api` block (§6).

## 18. Validation (forms + API)

**One schema, enforced on both sides.** Define it once in `app/schemas/<name>.ts`:

```ts
// app/schemas/login.ts
import { v } from "@wrnexus/validation";

export default v.object({
  email: v.string().email(),
  password: v.string().min(8, "Password must be at least 8 characters"),
});
```

**Builder API:**

- `v.string()` → `.min(n) .max(n) .length(n) .email() .url() .uuid() .date() .pattern(re,msg?)
.oneOf([...]) .trim()`
- `v.number()` → `.min(n) .max(n) .integer() .positive() .oneOf([...])`
- `v.boolean()`
- All types → `.optional() .label(text) .default(value) .refine(fn, msg?)` (⚠️ `refine` is
  **server‑only**, not mirrored to the client).
- `v.object(fields)` → `.parse(input)` → `{ ok, value, errors }`, and `.describe()` (JSON descriptor
  for the client).

**Server** — in an API handler:

```ts
import { parseBody } from "@wrnexus/validation";
import login from "../schemas/login.ts";

export async function POST(ctx: Context): Promise<Response> {
  const result = await parseBody(login, ctx.req); // reads JSON / urlencoded / multipart
  if (!result.ok) return result.response; // ready 400: { ok:false, errors:{field:msg} }
  const { email, password } = result.value as { email: string; password: string };
  // …
}
```

`parseEnv(schema, source?)` validates env vars and throws one readable multi‑line error.

**Client form flow** (zero JS you write). The schema's descriptor is baked into
`window.__wrnSchemas` by name (filename). WrNexus a form up with attributes:

```
<form data-schema="login" method="post" action="/api/login" data-redirect="/dashboard">
  <div data-component="input" type="email" name="email"></div>
  <span class="wrn-field-error" data-error="email"></span>

  <div data-component="input" type="password" name="password"></div>
  <span class="wrn-field-error" data-error="password"></span>

  <div class="wrn-alert wrn-alert--success" data-success="Signed in!" hidden></div>
  <button type="submit" class="wrn-btn wrn-btn--primary">Sign in</button>
</form>
```

- `data-schema="login"` — picks the descriptor.
- `name="…"` — matched to schema fields; validated on `blur` and `submit`.
- `data-error="field"` — receives the message; the input gets `aria-invalid` + `.wrn-invalid`.
- `data-success` — shown on success when there's no redirect.
- `data-redirect="/path"` (or `redirect` in the JSON response) — navigates on success.

On submit it validates client‑side, then POSTs `JSON` with `x-csrf-token` (from the `wrn-csrf`
cookie) and `credentials: same-origin`, and re‑surfaces server `errors` into the `data-error`
spans. It also dispatches `wrn:success` / `wrn:error` events. Scaffold: `wrnexus generate schema login`.

## 19. Database

Models in `app/db/schema.ts` are the **source of truth** for DDL, typing, and row mapping.
⚠️ **`ctx.db` does not exist yet** — use `getDb()` from `@wrnexus/db` in handlers.

### Define models

```ts
// app/db/schema.ts
import { v, table } from "@wrnexus/db";

export type User = { id: number; email: string; name: string; active: boolean; createdAt: Date };

export const users = table<User>("users", {
  id: v.id(), // auto-increment PK
  email: v.string().unique(),
  name: v.string(),
  active: v.boolean().default(true),
  createdAt: v.timestamp().default("now"), // "now" → CURRENT_TIMESTAMP
});
```

Column builders: `id, text, string(=text), int, number(=real), real, bool, boolean(=bool),
timestamp, json`. Modifiers: `.optional() .unique() .default(value|"now") .primaryKey()
.references(table, column="id")`. ⚠️ `export const` each table so the CLI can discover it.

### Migrations

```bash
wrnexus db new init --from-models   # scaffold up/down CREATE TABLE from schema.ts (topo-sorted)
wrnexus db migrate                  # apply pending (each in a transaction, recorded once)
wrnexus db status                   # [x]/[ ] applied
wrnexus db rollback                 # revert the last one
```

A migration is `.sql` split by markers:

```sql
-- +up
ALTER TABLE "users" ADD COLUMN "passwordHash" TEXT NOT NULL DEFAULT '';
-- +down
ALTER TABLE "users" DROP COLUMN "passwordHash";
```

In dev the server auto‑migrates at startup; in production run `wrnexus db migrate` explicitly.

### Typed queries

```sql
-- app/db/queries/users.sql
-- name: GetUserByEmail :one
SELECT * FROM users WHERE email = :email;

-- name: ListUsers :many
SELECT id, name, active FROM users ORDER BY name;

-- name: CreateUser :exec
INSERT INTO users (email, name, active) VALUES (:email, :name, :active);
```

`:one` → `Row | null`, `:many` → `Row[]`, `:exec` → `ExecResult`. `:param` placeholders become
positional. Run `wrnexus db generate` → `app/db/queries.gen.ts` with fully‑typed functions:

```ts
import { getDb } from "@wrnexus/db";
import { ListUsers, GetUserByEmail } from "../db/queries.gen.ts";

const all = await ListUsers(getDb()); // typed rows
const user = await GetUserByEmail(getDb(), { email }); // args object; User | null
```

(`SELECT *` rows are mapped through the model; aggregates like `COUNT(*) AS n` are typed `number`.)

### Runtime DB API

`getDb()` returns the process‑wide `Db` (set once at startup from `config.db`). `Db`:

```ts
db.all<T>(sql, params?, model?): Promise<T[]>
db.one<T>(sql, params?, model?): Promise<T | null>
db.exec(sql, params?): Promise<{ changes: number; lastInsertId?: number }>
db.tx(async (tx) => { … })            // transaction, rolls back on throw (nested reuses current)
db.createTable(model): Promise<void>  // CREATE TABLE IF NOT EXISTS
```

```ts
await getDb().exec("INSERT INTO users (email, name) VALUES (?, ?)", [email, name]);
const rows = await getDb().all("SELECT * FROM users", [], users); // User[]
```

### Pagination & relations

```ts
import { paginate, loadRelated } from "@wrnexus/db";

const page = await paginate(
  getDb(),
  { sql: "SELECT * FROM users ORDER BY name", model: users }, // no LIMIT — it's added
  { page: 2, perPage: 25 },
); // → { items, page, perPage, total, totalPages, hasNext, hasPrev }

const list = await getDb().all("SELECT * FROM users", []);
await loadRelated(getDb(), list, { table: "posts", foreignKey: "userId", as: "posts" }); // hasMany → user.posts
// belongsTo: { table:"users", localKey:"userId", foreignKey:"id", as:"author", single:true, model:users }
```

`loadRelated` batches with one `WHERE fk IN (…)` (no N+1).

### Seeding

```ts
// app/db/seed.ts   →  wrnexus db seed
import type { Db } from "@wrnexus/db";
export default async function seed(db: Db): Promise<void> {
  await db.exec("DELETE FROM users"); // make it re-runnable
  await db.exec("INSERT INTO users (email, name, active) VALUES (?, ?, ?)", [
    "ada@x.dev",
    "Ada",
    1,
  ]);
}
```

Config: `db: { driver: "sqlite" | "postgres" | "mysql" | "mongo", url }`; sqlite `file:` URLs
resolve relative to the app root. `wrnexus db studio [table]` lists tables/row counts or dumps rows.

### Multiple databases

Connect to as many databases as you want and read/write any of them per request. The
`db` setting is the **default**; add named connections under `databases`:

```ts
// wrnexus.config.ts
db: { driver: "sqlite", url: "file:./dev.db" },              // default → getDb()
databases: {
  analytics: { driver: "postgres", url: process.env.ANALYTICS_URL! }, // → getDb("analytics")
},
```

Reach them by name at runtime:

```ts
const users = await getDb().all("SELECT * FROM users"); // default
const hits = await getDb("analytics").all("SELECT * FROM events"); // named
```

Each named database has its **own** files under `app/db/<name>/` (`schema.ts`,
`migrations/`, `queries/` → `queries.gen.ts`, `seed.ts`). Target one with `--db=<name>`:

```bash
wrnexus db new init --from-models --db=analytics   # scaffold app/db/analytics/migrations
wrnexus db migrate --db=analytics                  # migrate the named db
wrnexus db generate --db=analytics                 # regenerate its typed queries
wrnexus db studio users --db=analytics             # inspect it
```

The generated query functions take a `Db` as their first argument, so pass the connection
you want: `await ListEvents(getDb("analytics"))`. In dev, every configured database is
auto-migrated at startup.

## 20. Auth, sessions & CSRF

Primitives from `@wrnexus/core`:

```ts
import {
  hashPassword,
  verifyPassword,
  logIn,
  logOut,
  getUser,
  requireAuth,
  verifyCsrf,
} from "@wrnexus/core";

await hashPassword("secret"); // argon2id (Bun.password)
await verifyPassword(plain, hash); // boolean
logIn(ctx, { id, email, name }); // regenerates session id (fixation defense), stores user
logOut(ctx); // clears session
getUser(ctx); // current user or null
```

**Login route** (canonical):

```ts
export async function POST(ctx: Context): Promise<Response> {
  if (!verifyCsrf(ctx)) return new Response("Invalid CSRF token", { status: 403 });
  const result = await parseBody(login, ctx.req);
  if (!result.ok) return result.response;
  const { email, password } = result.value as { email: string; password: string };
  const user = await GetUserByEmail(getDb(), { email });
  if (!user || !(await verifyPassword(password, user.passwordHash)))
    return Response.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
  logIn(ctx, { id: user.id, email: user.email, name: user.name }); // store only safe fields
  return Response.json({ ok: true });
}
```

**Guarding:** `requireAuth({ loginPath: "/login" })` as middleware — API/JSON → 401, page nav → 302
to `${loginPath}?next=<target>`. Or inline: `if (getUser(ctx) == null) …`.

**CSRF** (double‑submit): cookie `wrn-csrf` (JS‑readable), header `x-csrf-token`. `csrfToken(ctx)`
returns/creates the token; `verifyCsrf(ctx)` passes GET/HEAD/OPTIONS, else compares header to cookie.
The WrNexus UI form runtime sends the header automatically. Add `csrfProtection()` middleware to enforce
globally.

**Sessions:** cookie `wrnexus.sid` (HttpOnly, Lax, Secure on HTTPS), 24h sliding TTL, 256‑bit id.
Default backend is in‑memory. Swap it:

```ts
import { setSessionBackend } from "@wrnexus/core";
import { sqliteSessionStore } from "@wrnexus/db/session";
setSessionBackend(sqliteSessionStore("./sessions.db")); // sync backend
// async/Redis-style: use the loadSession(asyncBackend) middleware instead
```

## 21. Middleware

Each `app/middleware/*.ts` file `export default`s one middleware — a value or a function
`(ctx, next) => Response | Promise<Response>`. Return `next()` to continue or a `Response` to
short‑circuit. Scope a middleware to a path by checking `ctx.url.pathname` / `ctx.req.method`.

```ts
// app/middleware/ratelimit.ts
import { rateLimit, type Context, type Next } from "@wrnexus/core";
const limiter = rateLimit({ max: 5, windowMs: 60_000, message: "Slow down" });
export default async function (ctx: Context, next: Next) {
  if (ctx.req.method === "POST" && ctx.url.pathname === "/api/login") return limiter(ctx, next);
  return next();
}
```

Core factories and their key options:

- `rateLimit({ windowMs=60000, max=60, key?, trustProxy=false, message?, headers=true, store? })` —
  emits `RateLimit-*` headers + 429 with `Retry-After`.
- `requestLogger({ format="pretty"|"json", sink?, requestIdKey="requestId" })`.
- `csrfProtection()` — 403s unsafe requests failing CSRF.
- `sessionAuth()` — sets `ctx.user` from the session.
- `requireAuth({ loginPath="/login" })` — 401 (API) / 302 (page).
- `loadSession(backend, { ttlMs? })` — async session backend; register early.
- `authorize(policy)`, `requireRole(...roles)`, `requirePermission(rbac, permission)` from
  `@wrnexus/authz`.
- `jwtAuth({ secret, getToken?, required=true })` from `@wrnexus/jwt`.
- `createTracker({ sinks }).middleware()` from `@wrnexus/tracking`.

## 22. i18n

Opt‑in: add dictionaries under `app/locales/<lang>.json`. Language resolves per request from the
`wrn-lang` cookie → `Accept-Language` → default.

```json
// app/locales/en.json
{ "home": { "title": "Home", "intro": "Welcome" }, "api": { "greeting": "Hello" } }
```

In views: `{t:home.title}`. In handlers: `ctx.t("api.greeting")` and `ctx.lang`.

```ts
export const GET = async (ctx: Context) =>
  Response.json({ message: ctx.t("api.greeting"), lang: ctx.lang });
```

Config: `i18n: { default: "en", locales: ["en", "fr"] }` (both optional; inferred from files).
Switch language from the view with `data-wrn-lang-set="fr"`. Formatting helpers (Intl‑based) are
exported from `@wrnexus/i18n`: `formatNumber`, `formatCurrency`, `formatDate`, `formatRelativeTime`,
`plural`.

## 23. SEO

Per‑page `seo { }` merges over global `seo` config. Supported keys: `title`, `titleTemplate`
(`"%s | Site"`), `description`, `canonical`, `canonicalBase`, `robots`, `keywords`, `image`,
`siteName`, `type`, `locale`, `twitterCard`, `twitterSite`, `themeColor`. The server builds an
escaped `<head>` from the merged metadata.

## 24. Security headers & CORS

Secure defaults are on. Configure under `security` (§32 for the full shape):

```ts
security: {
  cors: { enabled: true, origin: ["http://localhost:5173"], credentials: true, maxAge: 600 },
  contentSecurityPolicy: { directives: { "script-src": ["'self'", "https://cdn.example.com"] } },
  hsts: { maxAge: 31536000 },       // on in prod by default
  frameOptions: "DENY",
}
```

Defaults include a strict CSP (`default-src 'self'`, `script-src 'self'`, `style-src 'self'
'unsafe-inline'`, `frame-ancestors 'none'`, …), `X-Frame-Options: DENY`, restrictive
Permissions‑Policy, COOP `same-origin`, and Referrer‑Policy. Set any directive/section to `false`
to remove it, or `security.headers = false` to disable all framework headers. WebSocket upgrades are
guarded against cross‑site hijacking (same‑origin + configured CORS origins + non‑browser clients).

## 25. Realtime

Author a room in `app/realtime/<name>.ts` → served at `ws://host/realtime/<name>`. Use `[room].ts`
for dynamic multi‑room.

```ts
// app/realtime/chat.ts
import { defineRoom } from "@wrnexus/core";

export default defineRoom({
  authorize: (info) => true, // { user?, query, headers } — false → 403
  onConnect(client) {
    client.broadcast({ type: "system", text: "joined", online: client.room.count() });
  },
  onMessage(client, message) {
    // JSON auto-parsed
    client.room.broadcast({ type: "message", user: client.user, text: String(message.text) });
  },
  onLeave(client) {
    client.broadcast({ type: "system", text: "left", online: client.room.count() - 1 });
  },
});
```

`client` API: `send(msg)` (this conn), `broadcast(msg)` (others), `client.room.broadcast(msg)`
(everyone), `client.to(id|ids).send(msg)`, `client.toUser(u|users).send(msg)`, `client.close()`.
Set `client.user = "u1"` to enable `toUser`. State: `client.data` (per conn), `client.room.state`
(shared while ≥1 connected); `client.room.count()`, `client.room.clients()`.

**Client view** (zero JS):

```
<div data-room="chat" data-room-user="Ada">
  <span data-room-status data-room-status-class="badge"></span>
  <div data-room-log></div>

  <template data-room-item="message"><div><strong>%user%</strong>: %text%</div></template>
  <template data-room-item="system"><em>%text% (%online% online)</em></template>

  <form data-room-send>
    <input name="text" data-room-reset placeholder="Say something">
    <button type="submit">Send</button>
  </form>
</div>
```

- `data-room="name"` connects; optional `data-room-user` → `?user=`.
- `[data-room-log]` receives messages; `[data-room-status]` reflects `connected|disconnected|error`
  (with `is-*` classes when `data-room-status-class` is set).
- `<template data-room-item="type">` renders each message of that `type`; `%field%` placeholders are
  **HTML‑escaped**. `data-room-item=""` is the fallback template.
- `<form data-room-send>` sends all named fields as one JSON object; `data-room-reset` fields clear
  after send.

### Realtime across multiple app processes

By default a room's `broadcast`/`toUser` reach only clients on **that** process. To make them reach
clients on **every** running app process/instance (multiple runs, or multiple apps behind the
gateway), enable realtime scaling — broadcasts are bridged over Redis pub/sub:

```ts
// wrnexus.config.ts
realtime: { scale: true, redisUrl: process.env.REDIS_URL }, // defaults to redis://localhost:6379
```

Now `client.room.broadcast(msg)` in one app instance is delivered to subscribers in all the
others. Connection‑targeted `send`/`to(id)` stay local (ids are per‑process). Under the hood it's
`bridgeRealtime(registry, bus)` from `@wrnexus/core` + the `@wrnexus/pubsub` Redis driver — you can
also wrn it manually for custom buses.

## 26. SSR/CSR data bindings

Fetch data for a page without hand‑writing fetch calls, using `ssr`/`client` blocks (§6) + `api="…"`
on an element:

```
page Users {
  ssr {
    api list GET /api/users/ssr { return users.map(u => u.name).join(", "); }
  }
  client {
    api live GET /api/users/csr { return $data.count; }
  }
  view {
    <p api="list">loading…</p>     <!-- SSR: rendered before HTML is sent -->
    <p api="live">…</p>            <!-- CSR: fetched after hydration -->
  }
}
```

- **`ssr`** bindings render on the server (replace an HTML comment marker with the value).
- **`client`** bindings render after hydration (the browser sees `data-wrnexus-csr` and fetches
  `/__wrnexus/csr?…`). Inside a binding body, `$data` is the response, and `cookies`/`session`/
  `localStorage` adapters are in scope.

## 27. Optional packages

Convenience helpers and dependency-free feature packages. Import only what you need.

- **`@wrnexus/helpers`** — context helpers including `getOriginalRequestUrl`,
  `getOriginalRequestOrigin`, `getOriginalRequestPath`, `getOriginalRequestMethod`, and the safe
  forward-auth `redirectToLogin(ctx, "/login", { allowedHosts })` response helper.
- **`@wrnexus/jwt`** — `signJwt(payload, secret, { expiresIn?, now? })`, `verifyJwt(token, secret)`
  (throws `JwtError`), `jwtAuth({ secret, getToken?, required? })` middleware.
- **`@wrnexus/oauth`** — PKCE OAuth 2.0. Presets `google/github/discord(creds)` + `defineProvider`.
  ```ts
  const p = google({ clientId, clientSecret });
  const { url, state, verifier } = await startAuth(p, { redirectUri }); // store state+verifier, 302 to url
  const { profile } = await completeAuth(p, { code, redirectUri, verifier }); // then logIn(ctx, profile)
  ```
- **`@wrnexus/authz`** — `defineRbac({ admin: ["post:*"] })`, `requireRole`, `requirePermission`,
  policy combinators `any/all/attr`, `authorize(policy)`.
- **`@wrnexus/encryption`** — `encrypt/decrypt(text, key)` (AES‑256‑GCM), `generateKey`, `deriveKey`
  (PBKDF2), `sha256`, `hmacSign/hmacVerify`.
- **`@wrnexus/pubsub`** — `createPubSub()` → `publish(topic, msg)` / `subscribe(pattern, handler)`
  (supports `ns:*` and `*`). Default driver is in‑process. For **cross‑process** messaging
  (multiple app runs / multiple domains) use the Redis driver — a self‑contained RESP client,
  no extra dependency:
  ```ts
  import { createPubSub } from "@wrnexus/pubsub";
  import { redisDriver } from "@wrnexus/pubsub/redis";
  const bus = createPubSub(redisDriver(process.env.REDIS_URL)); // defaults to redis://localhost:6379
  bus.subscribe("order:*", (msg, topic) => {
    /* any app process receives it */
  });
  await bus.publish("order:created", { id: 7 });
  ```
- **`@wrnexus/queue`** — `createQueue()` → `add(name, data, { delayMs?, maxAttempts?, repeat? })`,
  `process(name, handler)`; retries + backoff, recurring jobs, `drain()` for tests.
- **`@wrnexus/tracking`** — `createTracker({ sinks: [consoleSink] })` → `capture(err, ctx?)` and
  `middleware()`.

## 28. Config profiles & env

Run the same app under named profiles (dev/prod/uat/test/…):

```ts
// wrnexus.config.ts
profiles: {
  production: { db: { driver: "postgres", url: process.env.DATABASE_URL! } },  // auto-applied by `wrnexus build`
  uat:        { seo: { robots: "noindex,nofollow" } },
  test:       { db: { driver: "sqlite", url: "file:./test.db" } },
}
```

- **Select:** `--profile=<name>` on `dev`/`build`/`db`/`test`, or `WRNEXUS_PROFILE`. `build` defaults
  to `production`, `test` to `test`.
- **Merge:** `profiles.<name>` is **deep‑merged** over the base config (objects merge; arrays/scalars
  replace).
- **Env cascade** (low → high): `.env` < `.env.<profile>` < `.env.local` < `.env.<profile>.local`.
  ⚠️ **Real environment variables always win** over any `.env` file.
- `wrnexus profiles` lists profiles and which `.env` files exist.

## 29. Testing

Write app tests with `@wrnexus/test` (one import: `bun:test` primitives + helpers). Run with
`wrnexus test` (defaults to the `test` profile, loading `.env.test`).

```ts
// app/example.test.ts
import { test, expect, renderComponent, callRoute, createHarness } from "@wrnexus/test";

test("component renders", async () => {
  const html = await renderComponent(COUNTER_SRC, { start: 5, label: "Clicks" });
  expect(html).toContain("Clicks");
});

test("api handler", async () => {
  const { POST } = await import("./api/echo.ts");
  const res = await callRoute(
    POST,
    new Request("http://t/api/echo", { method: "POST", body: "{}" }),
  );
  expect(res.status).toBe(200);
});

test("full app", async () => {
  const app = await createHarness(import.meta.dir + "/.."); // boots on an ephemeral port
  const res = await app.fetch("/");
  expect(res.status).toBe(200);
  app.close();
});
```

Also `mountHtml(html)` — mounts server HTML in happy‑dom with the reactive runtime hydrated, to
assert on `data-for`/`data-show`/`data-text`.

```bash
wrnexus test            # once (test profile)
wrnexus test --watch    # re-run on change
```

## 30. Build & deploy

```bash
wrnexus build .         # → dist/server.js (self-contained, minified) + islands/reactive/theme/ui/styles + public/
bun dist/server.js     # honors PORT
```

Containerize:

```bash
wrnexus generate docker   # Dockerfile (multi-stage Bun), .dockerignore, docker-compose.yml (app + Postgres)
docker compose up --build
```

The Dockerfile builds with `oven/bun:1` → runs on `oven/bun:1-slim`, `EXPOSE 3000`, health‑checks
`/healthz`. Run migrations in production explicitly (`wrnexus db migrate`).

## Monorepo & multi‑domain SaaS

Build several interconnected WrNexus apps in one repo, and serve them behind one port routed by
domain — the foundation for multi‑domain SaaS.

```bash
wrnexus workspace acme       # scaffold a monorepo
cd acme && bun install
bun run dev                 # = wrnexus gateway → http://localhost:3000
```

The scaffold:

```
acme/
  wrnexus.workspace.ts   # maps each app → the domains it serves
  apps/
    web/                # a WrNexus app  → localhost, web.localhost
    admin/              # a WrNexus app  → admin.localhost
  packages/
    shared/             # @app/shared — shared code + a cross-app pubsub bus
```

`wrnexus.workspace.ts`:

```ts
import type { WorkspaceConfig } from "@wrnexus/cli/workspace";
export default {
  apps: [
    { name: "web", dir: "apps/web", domains: ["localhost", "web.localhost"] },
    { name: "admin", dir: "apps/admin", domains: ["admin.localhost"] },
  ],
} satisfies WorkspaceConfig;
```

**The gateway** (`wrnexus gateway`) boots each app as its **own process** (full isolation — its
own database registry, pubsub, in‑memory state) and reverse‑proxies HTTP **and WebSocket** by the
`Host` header. Add the hosts to your machine (`/etc/hosts`): `127.0.0.1 web.localhost admin.localhost`.
Because apps are process‑isolated, one deploy can serve many tenants/domains safely.

**Gateway security & auth.** The gateway is the edge, so it can enforce access control before any
request reaches an app — set `security` (gateway‑wide) and per‑app `auth` in `wrnexus.workspace.ts`:

```ts
export default {
  security: {
    trustedHostsOnly: true, // reject unknown domains (404)
    rateLimit: { max: 300, windowMs: 60_000 }, // per client IP → 429
    headers: true, // baseline edge security headers
    accessLog: true, // log host → app, method, path, status
    // forwardedHeaders: true,            // X-Forwarded-For/Host/Proto (default on)
  },
  apps: [
    { name: "web", dir: "apps/web", domains: ["localhost"] },
    {
      name: "admin",
      dir: "apps/admin",
      domains: ["admin.localhost"],
      auth: { basic: { user: "admin", pass: "…" } }, // HTTP Basic
      // auth: { allowIps: ["127.0.0.1", "::1"] },      // IP allowlist
      // auth: { forward: { url: "https://auth/verify" } }, // forward-auth (SSO): 2xx = allow
    },
  ],
} satisfies WorkspaceConfig;
```

Per‑app `auth` supports **HTTP Basic**, an **IP allowlist**, and **forward‑auth** (the gateway calls
your verify endpoint with the request's cookies/Authorization; a 2xx allows it — the hook for SSO).
The gateway also serves a health/status endpoint at **`/__gateway/health`**.

**Interconnecting apps:**

- **Shared code:** import a workspace package (`@app/shared`) from any app.
- **Runtime messaging:** the shared package exposes a pubsub `bus` backed by Redis, so an event
  published in one app reaches subscribers in another:
  ```ts
  import { bus } from "@app/shared";
  await bus.publish("tenant:created", { id }); // app A
  bus.subscribe("tenant:*", (msg) => { … });   // app B (needs Redis)
  ```
- **Shared databases:** point apps at the same `db`/`databases` in their configs.

Add another app any time: `wrnexus create apps/reports`, then list it in `wrnexus.workspace.ts`.

---

---

# Part IV — Reference

## 31. CLI reference

```
wrnexus dev [app-dir] [--port=3000] [--profile=<name>]   Dev server (live reload, auto-migrate, regen routes/queries)
wrnexus build [app-dir] [--profile=<name>]               Production bundle → dist/  (production profile by default)
wrnexus create <app-name>                                Scaffold a new app
wrnexus workspace <name>                                 Scaffold a monorepo (apps/* + shared packages/*)
wrnexus workspace add <name> [--domain=name.localhost]  Add an app to the current workspace
wrnexus gateway [--port=3000] [--prod]                   Serve every workspace app behind one port, routed by domain
wrnexus generate <type> <name>   (alias: g)              Scaffold: page | component | api | schema
wrnexus generate routes                                  Regenerate app/routes.gen.ts
wrnexus generate docker                                  Scaffold Dockerfile + compose
wrnexus eject <name...>                                  Copy a WrNexus UI component into app/components/
wrnexus db new [name] [--from-models] [--db=<name>]      Scaffold a migration (optionally from schema.ts)
wrnexus db generate [--db=<name>]                        Regenerate queries.gen.ts from queries/*.sql
wrnexus db migrate | rollback | status [--db=<name>]     Apply / revert / list migrations
wrnexus db seed [--db=<name>]                            Run the seed script
wrnexus db studio [table] [--db=<name>]                  List tables + counts, or dump a table
                                                        (--db=<name> targets a named database under app/db/<name>/)
wrnexus test [app-dir] [--watch] [--profile=test]        Run the app's tests (bun test, test profile)
wrnexus profiles [app-dir]                               List config profiles + their .env files
wrnexus help | --help | -h
```

`generate` types & aliases: `page`(`p`) → `app/pages/<name>.wrn`; `component`(`c`) →
`app/components/<name>.wrn`; `api`(`a`) → `app/api/<name>.ts`; `schema`(`s`) →
`app/schemas/<name>.ts`. Names may include slashes for nesting.

## 32. `wrnexus.config.ts` schema

`export default` an `AppConfig` (all fields optional). Searched: `wrnexus.config.{ts,js,mjs}`.

```ts
import type { AppConfig } from "@wrnexus/styles";

const config: AppConfig = {
  compatibilityDate: "2026-08-02",
  frameworkBehaviour: 1,
  head: [ '<link rel="stylesheet" href="…">' ],   // string | string[] → appended to every <head>

  seo: {                                           // SeoConfig (global defaults, merged per page)
    title, titleTemplate, description, canonical, canonicalBase, robots,
    keywords, image, siteName, type, locale, twitterCard, twitterSite, themeColor,
  },

  theme: {
    palette: "violet",
    default: "dark",                               // theme when no wrn-theme cookie
    themes: { light: { "color-primary": "#2563eb" }, dark: { "color-primary": "#6c8cff" } },
  },

  i18n: { default: "en", locales: ["en", "fr"] },  // both optional (inferred from app/locales)

  db: { driver: "sqlite" | "postgres" | "mysql" | "mongo", url: "file:./dev.db" },  // default → getDb()
  databases: {                                       // named connections → getDb("<name>")
    analytics: { driver: "postgres", url: "…" },     // files under app/db/analytics/
  },
  realtime: { scale: true, redisUrl: "…" },          // bridge room broadcasts across app processes

  port: 3000,                                        // ⚠️ dev reads port only from --port=, not this

  styles: {
    entry: "app/styles/global.css",                 // default
    process: async ({ entryPath, appDir, appRoot, mode }) => "/* final css */",  // optional
  },

  security: {                                        // SecurityConfig (secure defaults)
    headers: true,                                   // false → disable ALL framework headers
    cors: false | {                                  // opt-in
      enabled: true, origin: "*" | string | string[], methods, allowedHeaders,
      exposedHeaders, credentials, maxAge,
    },
    contentSecurityPolicy: false | { enabled, reportOnly, directives, useDefaults },
    hsts: false | { enabled, maxAge, includeSubDomains, preload },     // on in prod
    trustedTypes: false | { enabled, policyNames, requireForScript, allowDuplicates },
    crossOriginOpenerPolicy: false | "same-origin" | "same-origin-allow-popups" | "unsafe-none",
    frameOptions: false | "DENY" | "SAMEORIGIN",
    referrerPolicy: false | "strict-origin-when-cross-origin",
    permissionsPolicy: false | { camera: [], geolocation: [], fullscreen: ["self"], … },
    extraHeaders: { "X-Custom": "…" },
  },

  profiles: {                                        // deep-merged over base when active
    production: { /* Partial<AppConfig> */ },
    uat: { … }, test: { … },
  },
};
export default config;
```

**CSP defaults:** `default-src 'self'`; `script-src 'self'`; `style-src 'self' 'unsafe-inline'`;
`img-src 'self' data: blob:`; `font-src 'self' data:`; `connect-src 'self' ws: wss:`;
`object-src 'none'`; `base-uri 'self'`; `frame-ancestors 'none'`; `form-action 'self'`. Set a
directive to `false`/`null` to remove it.

## 33. Directive & attribute cheat sheet

**Reactive (client runtime):** `data-scope="k: v, …"` · `data-on-<event>` (via `@event`) ·
`data-text` · `data-show` · `data-for="item[, i] in list"` · `data-wrnexus-csr` (generated).
**Server/compiler:** `data-component` · `data-slot` · `<slot [name]>` · `{expr}` · `{{expr}}` ·
`{t:key}` (→ `data-t`) · `@event="stmt"` · `api="binding"`.
**Forms:** `data-schema` · `name` · `data-error` · `data-success` · `data-redirect`.
**Theme:** `data-wrn-theme-toggle` · `data-wrn-theme-set="name"`.
**i18n:** `data-wrn-lang-set="lang"` · `data-wrn-lang`.
**Realtime:** `data-room` · `data-room-user` · `data-room-log` · `data-room-status`
(+`data-room-status-class`) · `<template data-room-item="type">` (`%field%`) · `data-room-send` ·
`data-room-reset`.

## 34. Package API index

**Core / always present**

- `@wrnexus/core` — `Context`, `createContext`; security (`escapeHtml`, `isSafeRequestPath`); CSRF
  (`csrfToken`, `verifyCsrf`, `csrfProtection`, `CSRF_COOKIE`, `CSRF_HEADER`); auth (`hashPassword`,
  `verifyPassword`, `logIn`, `logOut`, `getUser`, `sessionAuth`, `requireAuth`); middleware
  (`rateLimit`, `requestLogger`); cache (`TTLCache`, `cacheControl`, `etag`, `notModified`); uploads
  (`saveUpload`, `collectUploads`, `sanitizeFilename`); streaming (`streamResponse`, `sse`); realtime
  (`defineRoom`, `createRealtimeRegistry`, `bridgeRealtime`); errors (`renderError`, `renderNotFound`); headers
  (`withSecurityHeaders`, CORS); sessions (`setSessionBackend`, `loadSession`); JSX runtime.
- `@wrnexus/compiler` — `compileWrnFile`, `compile`, `parse`, `generate`, `Lexer`, `ParseError`.
- `@wrnexus/router` — `buildRouter`, `matchRoute`, `generateRoutesFile`.
- `@wrnexus/ssr` — `renderDocument`.
- `@wrnexus/csr` — `getReactiveRuntime`, `getNavRuntime`, `getRealtimeRuntime`.
- `@wrnexus/reactive` — `signal`.
- `@wrnexus/styles` — `loadAppConfig`, `loadRawConfig`, `resolveProfile`, `loadEnv`, `headToString`,
  `findStyleEntry`, `bundleCss`, `renderStyles`, theme helpers, `DEFAULT_THEMES`, `AppConfig`.
- `@wrnexus/dev-server` — `startServer(opts) → { url, router, stop }`, `createProductionServer`,
  node adapter.
- `@wrnexus/cli` — the `wrnexus` binary.

**Feature packages**

- `@wrnexus/db` — `v`, `table`, `getDb(name?)`/`setDb`/`registerDb`/`hasDb`/`databaseNames`,
  `createDb`, `paginate`, `loadRelated`, migrations, query generation; subpaths `/session`,
  `/sqlite`, `/postgres`, `/mysql`, `/mongo`.
- `@wrnexus/dev-server` — `startServer`, `startGateway` (multi-app host router), prod handler.
- `@wrnexus/cli` — the `wrnexus` binary; subpath `/workspace` (`WorkspaceConfig`, `startGateway` integration).
- `@wrnexus/validation` — `v`, `parseBody`, `parseEnv`, `invalid`, `renderSchemasScript`.
- `@wrnexus/helpers` — original gateway URL/path/method helpers and `redirectToLogin`.
- `@wrnexus/ui` — `uiComponentsDir`, `uiCssPath`, `uiCss`, `uiComponentNames`.
- `@wrnexus/i18n` — `loadLocales`, `resolveI18n`, `makeT`, `resolveLang`, `translateHtml`,
  `formatNumber`, `formatCurrency`, `formatDate`, `formatRelativeTime`, `plural`.
- `@wrnexus/pubsub` — `createPubSub`, `memoryDriver`; subpath `/redis` → `redisDriver(url?)` for
  cross-process messaging.
- `@wrnexus/jwt`, `@wrnexus/oauth`, `@wrnexus/authz`, `@wrnexus/encryption`, `@wrnexus/queue`,
  `@wrnexus/tracking` — see §27.
- `@wrnexus/test` — `renderComponent`, `mountHtml`, `callRoute`, `createHarness` + `bun:test`.

## 35. The `Context` object

Available in API handlers, page `api` blocks, and middleware (`ctx`):

- `ctx.req: Request`, `ctx.url: URL`, `ctx.params: Record<string,string>` (dynamic route segments).
- `ctx.session` — get/set/clear session values; `ctx.user` — the logged‑in user (or null).
- `ctx.t(key, params?)` — translate; `ctx.lang` — active language.
- `ctx.locals` — per‑request scratch space (e.g. `requestId`).
- Helpers: `verifyCsrf(ctx)`, `csrfToken(ctx)`, `getUser(ctx)`, `logIn/logOut(ctx, …)`.
- DB: use `getDb()` (⚠️ not `ctx.db`).

## 36. Editor support (VS Code)

The `editors/vscode` extension provides `.wrn` highlighting (embedded HTML/CSS/TypeScript), live
diagnostics from the real compiler, snippets, completions, and **distinct colors for WrNexus's own
attributes** (`@event`/`{t:}` in one accent, `data-*` directives in another). See
`editors/vscode/README.md`. The repo's `.vscode/settings.json` applies those attribute colors when
you edit `.wrn` files here.

---

_This guide is generated to be self‑contained. When in doubt, the source of truth is the code under
`packages/*` and the runnable reference app under `examples/basic-app`._
