Files
WRNexusJS/docs/GUIDE.md
T

1413 lines
58 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 **stepbystep**
> path from `create` to a full app: pages, components, reactivity, APIs, validation, database,
> auth, i18n, theming, realtime, testing, and deploy.
WrNexus is an **SSRfirst**, **Bunfirst** fullstack framework. Pages render to HTML on the
server; interactivity is added by **serverrendered components** hydrated in the browser by a
single generic runtime (no percomponent 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 <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 conventionbased: 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 shortcircuit.
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** toplevel 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 |
| Nonstate `{expr}` in view | live **client mustache** | **baked serverside** (HTMLescaped) |
| 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 baretoendofline.
### `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 { }` — 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 uppercased; path is autoprefixed to `/api` and traversalchecked. `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, selfclosing `<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>`; noJS clients see
the real value, the runtime keeps it live.
- **Page + nonstate** → left as a literal mustache; the runtime fills it at hydration.
- **Component + prop/constant** → baked serverside (HTMLescaped), **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 **CSPsafe, evalfree** 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 handwrite it. |
| `data-show` | `data-show="open"` | Toggles `display` on truthiness. **Handauthored** (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; peritem `data-text`, `data-on-*`, attribute mustaches, and text mustaches are filled. **Handauthored.** |
| `data-component` | `data-component="counter"` | Mounts a component (serverrendered, then hydrated). See §12. |
| `data-slot` | `<div data-slot="header">…</div>` | Fills a named `<slot name="header">` of a component (see §8). |
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" data-text="i.t"></li>
</ul>
</div>
}
```
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 {
<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 oneperline** (the value is read to endofline). 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**; toplevel `api` does not.
- **`props` on a `page` are ignored** — they only matter for `component`.
- **Reserved JS words** as prop/state names are fine (autorenamed internally in components).
---
---
# Part III — Build an app, feature by feature
Each step is selfcontained. 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`:
```
component 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.
## 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 rerenders only what changed. No `new Function`,
no `eval` — CSPsafe.
## 14. Wire UI
`@wrnexus/ui` ships **24** themed components, autodiscovered 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`).
```
<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 `--wire-*`, (2) redefine `.wire-*` 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 `--wire-<key>` CSS variables (server + client). Builtin
`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" },
},
}
```
Builtin 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-wire-theme-toggle>Toggle theme</button>
<button data-wire-theme-set="light">Light</button>
```
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
builtin 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
**serveronly**, 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 multiline 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:
```
<form data-schema="login" method="post" action="/api/login" data-redirect="/dashboard">
<div data-component="input" type="email" name="email"></div>
<span class="wire-field-error" data-error="email"></span>
<div data-component="input" type="password" name="password"></div>
<span class="wire-field-error" data-error="password"></span>
<div class="wire-alert wire-alert--success" data-success="Signed in!" hidden></div>
<button type="submit" class="wire-btn wire-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` + `.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 clientside, then POSTs `JSON` with `x-csrf-token` (from the `wire-csrf`
cookie) and `credentials: same-origin`, and resurfaces 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<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 automigrates 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 fullytyped 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 processwide `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** (doublesubmit): cookie `wire-csrf` (JSreadable), 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, 256bit id.
Default backend is inmemory. 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
shortcircuit. 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
Optin: add dictionaries under `app/locales/<lang>.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 (Intlbased) are
exported from `@wrnexus/i18n`: `formatNumber`, `formatCurrency`, `formatDate`, `formatRelativeTime`,
`plural`.
## 23. SEO
Perpage `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
PermissionsPolicy, COOP `same-origin`, and ReferrerPolicy. Set any directive/section to `false`
to remove it, or `security.headers = false` to disable all framework headers. WebSocket upgrades are
guarded against crosssite hijacking (sameorigin + configured CORS origins + nonbrowser clients).
## 25. Realtime
Author a room in `app/realtime/<name>.ts` → served at `ws://host/realtime/<name>`. Use `[room].ts`
for dynamic multiroom.
```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
**HTMLescaped**. `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. Connectiontargeted `send`/`to(id)` stay local (ids are perprocess). Under the hood it's
`bridgeRealtime(registry, bus)` from `@wrnexus/core` + the `@wrnexus/pubsub` Redis driver — you can
also wire it manually for custom buses.
## 26. SSR/CSR data bindings
Fetch data for a page without handwriting 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)` (AES256GCM), `generateKey`, `deriveKey`
(PBKDF2), `sha256`, `hmacSign/hmacVerify`.
- **`@wrnexus/pubsub`** — `createPubSub()` → `publish(topic, msg)` / `subscribe(pattern, handler)`
(supports `ns:*` and `*`). Default driver is inprocess. For **crossprocess** messaging
(multiple app runs / multiple domains) use the Redis driver — a selfcontained 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 **deepmerged** 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 happydom 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`, healthchecks
`/healthz`. Run migrations in production explicitly (`wrnexus db migrate`).
## Monorepo & multidomain SaaS
Build several interconnected WrNexus apps in one repo, and serve them behind one port routed by
domain — the foundation for multidomain 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, inmemory state) and reverseproxies 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 processisolated, 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` (gatewaywide) and perapp `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;
```
Perapp `auth` supports **HTTP Basic**, an **IP allowlist**, and **forwardauth** (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 Wire 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 = {
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 wire-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-wire-theme-toggle` · `data-wire-theme-set="name"`.
**i18n:** `data-wire-lang-set="lang"` · `data-wire-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` — `compileWireFile`, `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` wiring).
- `@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 loggedin user (or null).
- `ctx.t(key, params?)` — translate; `ctx.lang` — active language.
- `ctx.locals` — perrequest 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 selfcontained. When in doubt, the source of truth is the code under
`packages/*` and the runnable reference app under `examples/basic-app`._