# 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. Wire 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 ` 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 ); 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 .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 `` 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 `` 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 { start = 0 // default's TYPE drives coercion: start="5" arrives as number 5 label = "Count" // string disabled = false // boolean } ``` ### `state` — ⚠️ one per line ``` state count = start // may reference a prop or earlier state state total = 0 ``` State seeds the reactive scope. On pages, seeds are also evaluated at compile time for SSR baking. ### `view { }` ``` view { … } ``` 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(--wire-color-surface); border-radius: var(--wire-radius); } } ``` Multiple `style` blocks accumulate. Use theme tokens (`var(--wire-*)`) so styles restyle on theme change. ### `functions { }` — shared **server‑only** helpers ``` functions { function slug(s) { return s.toLowerCase().replace(/\s+/g, "-"); } } ``` Available to `api`, `ssr`/`client`, and `realtime` bodies. ### `api { }` — 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 { }` and `functions { }`. Reference a binding from the view with `api="userList"` on an element. ### `realtime { on (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/.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 ``, void elements (`
`, ``, …), and `` (dropped) all work. **Attribute values must be quoted.** A valueless attribute is boolean (` ``` `@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-` | `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 `display` on truthiness. **Hand‑authored** (no `{}` sugar). | | `data-for` | `data-for="item in items"` or `data-for="item, i in items"` | Repeats the element per list item. Inside, `item`/`i` are locals; per‑item `data-text`, `data-on-*`, attribute mustaches, and text mustaches are filled. **Hand‑authored.** | | `data-component` | `data-component="counter"` | Mounts a component (server‑rendered, then hydrated). See §12. | | `data-slot` | `
` | Fills a named `` of a component (see §8). | Example — a reactive list you write by hand: ``` view {
} ``` There are also framework attributes consumed by **other** runtimes (loaded only when present): `data-wire-theme-toggle`/`data-wire-theme-set` (theme, §15), `data-schema`/`data-error`/ `data-success`/`data-redirect` (forms, §18), `data-wire-lang-set`/`data-wire-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 {
} } ``` ``` // a page using it page Reports { layout = "dashboard" view {

Reports

} } ``` `fallback` 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 {

Hello WrNexus

About

} } ``` Scaffold: `wrnexus generate page home` (alias `g p`). ## 11. Layouts `app/layouts/public.wrn`: ``` component Public { view {
} } ``` A page opts in with `layout = "public"`. Missing → a `default` layout if present; `layout = "none"` disables layouts for that page. ## 12. Components & props `app/components/counter.wrn`: ``` component Counter { props { start = 0 label = "Count" } state count = start view { } } ``` Mount it in any page/view — pass props as attributes: ```
``` **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 {

Count doubled: {count * 2}

} } ``` The runtime tracks dependencies automatically and re‑renders only what changed. No `new Function`, no `eval` — CSP‑safe. ## 14. Wire 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 `--wire-*` tokens + `.wire-*` 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`). ```
``` **Override styling**, in priority order: (1) theme tokens `--wire-*`, (2) redefine `.wire-*` in your CSS (loads after `ui.css`), (3) the `class` prop, (4) `wrnexus eject ` 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 `--wire-` CSS variables (server + client). Built‑in `light` and `dark`; the cookie `wire-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: ``` ``` The client exposes `window.wireTheme = { 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: ['']`. - **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/.ts` → `/api/`. 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/.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 { 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.__wireSchemas` by name (filename). Wire a form up with attributes: ```
``` - `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` + `.wire-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 `wire-csrf` cookie) and `credentials: same-origin`, and re‑surfaces server `errors` into the `data-error` spans. It also dispatches `wire:success` / `wire: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("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(sql, params?, model?): Promise db.one(sql, params?, model?): Promise 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 // 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 { 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//` (`schema.ts`, `migrations/`, `queries/` → `queries.gen.ts`, `seed.ts`). Target one with `--db=`: ```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 { 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=`. Or inline: `if (getUser(ctx) == null) …`. **CSRF** (double‑submit): cookie `wire-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 Wire 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`. 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/.json`. Language resolves per request from the `wire-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-wire-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 `` 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/.ts` → served at `ws://host/realtime/`. 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): ```
``` - `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). - `