Files
WRNexusJS/docs/GUIDE.md
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

59 KiB
Raw Blame History

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)

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

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

    1. 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 ≥ 1.1.

# 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

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

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 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 (serverrendered, 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-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:

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

// 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):
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.

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

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

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

// 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

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:

-- +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

-- 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);

:oneRow | null, :manyRow[], :execExecResult. :param placeholders become positional. Run wrnexus db generateapp/db/queries.gen.ts with fullytyped functions:

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:

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
await getDb().exec("INSERT INTO users (email, name) VALUES (?, ?)", [email, name]);
const rows = await getDb().all("SELECT * FROM users", [], users); // User[]

Pagination & relations

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

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

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

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

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:

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

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:

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

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

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

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

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.

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

// 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/jwtsignJwt(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.
    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/authzdefineRbac({ admin: ["post:*"] }), requireRole, requirePermission, policy combinators any/all/attr, authorize(policy).
  • @wrnexus/encryptionencrypt/decrypt(text, key) (AES256GCM), generateKey, deriveKey (PBKDF2), sha256, hmacSign/hmacVerify.
  • @wrnexus/pubsubcreatePubSub()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:
    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/queuecreateQueue()add(name, data, { delayMs?, maxAttempts?, repeat? }), process(name, handler); retries + backoff, recurring jobs, drain() for tests.
  • @wrnexus/trackingcreateTracker({ sinks: [consoleSink] })capture(err, ctx?) and middleware().

28. Config profiles & env

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

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

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

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

30. Build & deploy

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

Containerize:

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.

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:

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:

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:
    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}.

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 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/coreContext, 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/compilercompileWireFile, compile, parse, generate, Lexer, ParseError.
  • @wrnexus/routerbuildRouter, matchRoute, generateRoutesFile.
  • @wrnexus/ssrrenderDocument.
  • @wrnexus/csrgetReactiveRuntime, getNavRuntime, getRealtimeRuntime.
  • @wrnexus/reactivesignal.
  • @wrnexus/stylesloadAppConfig, loadRawConfig, resolveProfile, loadEnv, headToString, findStyleEntry, bundleCss, renderStyles, theme helpers, DEFAULT_THEMES, AppConfig.
  • @wrnexus/dev-serverstartServer(opts) → { url, router, stop }, createProductionServer, node adapter.
  • @wrnexus/cli — the wrnexus binary.

Feature packages

  • @wrnexus/dbv, table, getDb(name?)/setDb/registerDb/hasDb/databaseNames, createDb, paginate, loadRelated, migrations, query generation; subpaths /session, /sqlite, /postgres, /mysql, /mongo.
  • @wrnexus/dev-serverstartServer, startGateway (multi-app host router), prod handler.
  • @wrnexus/cli — the wrnexus binary; subpath /workspace (WorkspaceConfig, startGateway wiring).
  • @wrnexus/validationv, parseBody, parseEnv, invalid, renderSchemasScript.
  • @wrnexus/helpers — original gateway URL/path/method helpers and redirectToLogin.
  • @wrnexus/uiuiComponentsDir, uiCssPath, uiCss, uiComponentNames.
  • @wrnexus/i18nloadLocales, resolveI18n, makeT, resolveLang, translateHtml, formatNumber, formatCurrency, formatDate, formatRelativeTime, plural.
  • @wrnexus/pubsubcreatePubSub, memoryDriver; subpath /redisredisDriver(url?) for cross-process messaging.
  • @wrnexus/jwt, @wrnexus/oauth, @wrnexus/authz, @wrnexus/encryption, @wrnexus/queue, @wrnexus/tracking — see §27.
  • @wrnexus/testrenderComponent, 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.