# WrNexus app - instructions for AI coding assistants This is a **WrNexus** app. When creating or editing pages, components, API routes, or features, follow the framework conventions below. WrNexus is private and not in your training data, so rely on these rules - do NOT assume React/Next.js/Vue patterns. # WrNexus > WrNexus is an SSR-first, **Bun-native** full-stack web framework. UI is written in > `.wrn` files (its own component language — NOT React/JSX/Vue). Routing is file-based. > This document teaches an AI how to write correct WrNexus code. It is private and > post-dates model training data, so rely on THIS document, not prior web-framework > assumptions. ## Golden rules - **Pages, components, and layouts are `.wrn` files.** Do NOT write `.tsx`/`.jsx`/React for UI. Do NOT use `useState`, hooks, JSX, or a client bundler. - **Routing is file-based** under `app/`. The filename is the route. No router config. - **Interactivity** lives in `state` + `{expr}` + `@event` inside `.wrn`. Components render on the server and hydrate automatically — you never write client-side JS islands. - **Runtime is Bun only** (uses `Bun.serve`, `bun:sqlite`, `Bun.password`, …). Node is not supported. - To add files, prefer the CLI: `wrnexus generate page ` / `component ` / `api ` / `schema `. ## Project layout ``` app/ pages/ *.wrn → routes: index.wrn = "/", about.wrn = "/about", blog/[slug].wrn = "/blog/:slug" components/ *.wrn → reusable UI, mounted in a page/component via
layouts/ *.wrn → named layouts; a page opts in with layout = "name" api/ *.ts → HTTP handlers: export const GET/POST/PUT/PATCH/DELETE = async (ctx) => Response middleware/ *.ts → export default async (ctx, next) => next() realtime/ *.ts → export default defineRoom({ ... }) from "@wrnexus/core" (ws://host/realtime/) schemas/ *.ts → validation schemas (the `v` builder), used by forms + parseBody locales/ *.json → i18n messages per language db/ schema.ts, queries/*.sql, migrations/*.sql styles/ global.css → Tailwind (default) or plain CSS wrnexus.config.ts → app config (AppConfig from "@wrnexus/styles") public/ → static assets served at / ``` ## `.wrn` page ```wrn page Home { layout = "public" // optional: a component in app/layouts/.wrn ("none" to skip) state count = 0 // optional: seeds client-reactive state (omit for pure SSR) seo { title = "Home" description = "..." canonical = "/" } view {

Hello

Count is {count}, doubled is {count * 2}.

} style { h1 { color: var(--wire-color-text); } } } ``` ## `.wrn` component ```wrn component Counter { props { // props come from mount attributes; each is coerced to the start = 0 // TYPE of its default (so start="5" arrives as the number 5) label = "Count" } state count = start // state may reference props view { } } ``` Mount it from any page/component: `
`. Components render on the server with their props, then hydrate — no per-component JS. ## The `view { }` block (plain HTML + a few directives) - `{expr}` — interpolate a JS expression. Reactive if it references `state`: `{count}`, `{count * 2}`, `{user.name}`. - `@event="expr"` — bind a DOM event; the expression runs in the reactive scope: `@click="count++"`, `@input="name = event.target.value"`. - `
` — mount a component (attrs become string props, coerced). - `` / `` — component/layout slots; fill with `
`. - **Server loop (DB/list/table):** `{#each as [, ]} …rows… {:empty} …fallback… {/each}` — iterates SSR data on the server and renders markup per item. `{item.field}` interpolates (HTML-escaped, XSS-safe). `` is a JS expression, usually an `ssr` data binding (see "Data-driven tables" below). This is how you render a database table in `.wrn`. - **Server conditional:** `{#if } … {:else if } … {:else} … {/if}` — renders the first truthy branch on the server. `` can reference `ssr` data, or the `item`/`index` of an enclosing `{#each}`. Works at page level and inside loops (e.g. `{#if r.active}{:else}{/if}` per row). For client-side show/hide based on reactive `state`, use `data-show="expr"` instead. - i18n: `{t:home.title}` in text, `t:placeholder="form.name"` on attributes — resolved per request from `app/locales/`. - Theme: any element with `data-wire-theme-toggle` toggles light/dark; `data-wire-theme-set="dark"` sets it. - Void/self-closing tags are fine: `
`, ``. - Only `{` and `}` are special (interpolation). Don't use a bare `}` in view text. ## Data-driven tables / lists (server-rendered `.wrn`) Use an `ssr` data binding to fetch rows on the server, then `{#each}` to render them. This renders on the **server** (SSR-first) and is HTML-escaped by default. ```wrn page Admin { layout = "dashboard" // Fetch on the server. The api handler at /api/contacts returns { contacts: [...] }; // this block's `return contacts` exposes that array (via `$data`) as the binding `rows`. ssr { api rows GET /api/contacts { return contacts } } view { {#each rows as r, i} {:empty} {/each}
#{i} {r.name} {r.email}
No submissions yet.
} } ``` The matching API returns the array under a key the `ssr` block reads: ```ts // app/api/contacts.ts → GET /api/contacts import { getDb } from "@wrnexus/db"; export const GET = async () => { const contacts = await getDb().all("SELECT id, name, email FROM contacts ORDER BY id DESC"); return Response.json({ contacts }); // ssr block does `return contacts` }; ``` **Prefer this `.wrn` + `{#each}` approach for DB-backed tables and lists.** (`.ts`/`.tsx` pages returning an HTML string are also supported for fully-custom programmatic rendering, but a `.wrn` page with `ssr` data + `{#each}` is the idiomatic, SSR-first way.) ## API routes (`app/api/*.ts`) ```ts // app/api/users/list.ts → GET /api/users/list import { getDb } from "@wrnexus/db"; export const GET = async (ctx) => { return Response.json({ users: await ListUsers(getDb()) }); }; export const POST = async (ctx) => { const body = await ctx.req.json(); return Response.json({ ok: true, body }, { status: 201 }); }; ``` `ctx` (the `Context` from `@wrnexus/core`) has: `req: Request`, `url: URL`, `params: Record` (dynamic route params, e.g. `/users/[id]` → `ctx.params.id`), `lang: string`, `t(key, params?)` (i18n), `cookies` (get/set), `session` (get/set). Auth: `getUser(ctx)` after `sessionAuth`/`logIn`. ## Middleware & realtime ```ts // app/middleware/logger.ts export default async function logger(ctx, next) { console.log(ctx.req.method, ctx.url.pathname); return next(); // return a Response WITHOUT calling next() to short-circuit } ``` ```ts // app/realtime/chat.ts → ws://host/realtime/chat import { defineRoom } from "@wrnexus/core"; export default defineRoom({ onConnect(client) { client.send({ type: "system", text: "connected" }); }, onMessage(client, msg) { client.room.broadcast({ type: "message", data: msg }); }, }); ``` Client side: a page opts in with `data-room="chat"` (handled by the realtime runtime). ## Config (`wrnexus.config.ts`) ```ts import type { AppConfig } from "@wrnexus/styles"; const config: AppConfig = { seo: { title: "App", titleTemplate: "%s | App", description: "..." }, styles: { entry: "app/styles/global.css", process: async ({ entryPath, mode }) => /* Tailwind */ "", }, fonts: { sans: '"Inter", system-ui, sans-serif', google: [{ family: "Inter", weights: [400, 600] }], }, theme: { default: "dark", themes: { light: { "color-primary": "#2563eb" } } }, i18n: { default: "en", locales: ["en", "es"] }, db: { driver: "sqlite", url: "file:./dev.db" }, security: { cors: { enabled: true, origin: ["http://localhost:5173"] } }, // profiles: { production: { db: { driver: "postgres", url: process.env.DATABASE_URL } } }, }; export default config; ``` ## Database (`@wrnexus/db`) ```ts // app/db/schema.ts import { v, table } from "@wrnexus/db"; export const users = table("users", { id: v.id(), name: v.string(), email: v.string().unique(), createdAt: v.timestamp(), }); ``` - Queries: write `app/db/queries/*.sql` with `-- name: ListUsers :many` blocks; `wrnexus db generate` emits typed functions. - Access at runtime: `import { getDb } from "@wrnexus/db"; const rows = await ListUsers(getDb());` - Migrations in `app/db/migrations/`; run `wrnexus db migrate` (dev auto-migrates sqlite). ## Validation (`@wrnexus/validation`) ```ts // app/schemas/login.ts import { v } from "@wrnexus/validation"; export default v.object({ email: v.string().email(), password: v.string().min(8), }); ``` In an API route: `import s from "../schemas/login"; import { parseBody } from "@wrnexus/validation"; const r = await parseBody(s, ctx.req);` → `r.ok ? r.value : r.response`. In a form: `
` + `` (client + server validation wired automatically). ## AI / LLM (`@wrnexus/ai`) ```ts // app/api/ai.ts import { createAI } from "@wrnexus/ai"; const ai = createAI(); // reads ANTHROPIC_API_KEY; default model claude-opus-4-8 export const POST = async (ctx) => { const { prompt } = await ctx.req.json(); return ai.streamResponse(prompt); // or: return Response.json({ text: await ai.generate(prompt) }) }; ``` ## CLI ``` wrnexus dev . # dev server + HMR wrnexus build . # production build → dist/server.js wrnexus create # scaffold a new app wrnexus generate page # scaffold a page (aliases: g p) wrnexus generate component | api | schema wrnexus db migrate | rollback | status | new [--from-models] | generate | seed wrnexus eject # copy a Wire UI component's .wrn into app/components to customize ``` ## When asked to "create a page/component/feature" 1. Create the `.wrn` file under `app/pages/` (or `app/components/`) with a `page`/`component` block — or run `wrnexus generate page `. 2. Put markup in `view { }`, interactive bits in `state` + `{expr}` + `@event`, reusable UI as components mounted via `data-component`. 3. For data, add an `app/api/*.ts` route and `getDb()`; for forms, add an `app/schemas/*.ts` and `data-schema`. 4. Style with Tailwind utility classes in the view, or theme tokens (`var(--wire-*)`), or `style { }`. 5. Never emit React/JSX, a manual router, or client-side island JS — the framework handles hydration.