# More Patterns Worked fragments for the things most apps need next: sessions, forms, validation, email, and scheduled work. Things the example above doesn't show. Each subsection points to its topic for the full story. ### RPC Use `/** @rpc */` to make a function callable from the browser. The compiler rewrites browser call sites into network requests and removes the body from the browser bundle. Call them like normal functions: no `await`, no `async`, no fetch. Arguments and return types are checked end-to-end. ```typescript // app/pages/home/services.ts import { sql, ValidationError } from "@elements/app"; import { Comment } from "./models"; /** @rpc */ export function searchComments(query: string): Comment[] { if (query.length < 2) { throw new ValidationError("query too short"); } return sql(` select * from comments where text ilike ${"%" + query + "%"} order by createdAt desc limit 20 `).all(); } ``` `@rpc` is the boundary between browser-reachable and server-only code. Once execution crosses into an rpc, anywhere downstream (the rpc body, helpers it calls, helpers those call) can call `sql()`, `tx()`, `session.login()` freely. Browser-reachable code that calls them is a compile error pointing at the call site. Rpc go in `services.ts` next to a page, or in `app/shared/services/.ts` for cross-page use. Never in a page's `index.ts`. Full topic: `elements man rpc`. ### SQL and Transactions ```typescript import { sql, tx } from "@elements/app"; let user = sql(`select * from users where id = ${id}`).first(); let users = sql(`select * from users where active = ${true}`); for (let u of users) { /* ... */ } tx(() => { let u = sql(`insert into users (name) values (${name}) returning *`).firstOrThrow("insert returned no row"); sql(`insert into auditLog (event, userId) values ('user_created', ${u.id})`); }); ``` `${value}` interpolations auto-extract as parameterized arguments at build time; SQL injection is impossible by construction. `sql(...)` returns a `SqlResult`: `.all()` for the array, `.first()` for `T | undefined`, `.firstOrThrow(msg)` for a required row. `tx(fn)` commits on return, rolls back on throw. Every `sql()` inside joins the same connection. Full topic: `elements man database`. ### Auth, Errors, Validation ```typescript import { session, AuthError, NotFoundError, ForbiddenError, ValidationError, } from "@elements/app"; session.isLoggedInOrThrow(); // throws AuthError if not logged in if (!isAdmin(session.getOrThrow("userId"))) { throw new ForbiddenError(); } throw new NotFoundError(`room ${id} not found`); // 404 throw new ForbiddenError(); // 403 throw new ValidationError("qty must be positive"); // 422, single rule throw new ValidationError({ email: ["must be a valid email"] }); // 422, per-field ``` `AppError` subclasses are `safe = true`: their messages reach the browser. Internal errors are wrapped in `ServerError` before being sent. - `AuthError` (401): unauthenticated - `ForbiddenError` (403): authenticated but not permitted - `NotFoundError` (404): resource missing - `ValidationError` (422): invalid input (string or `FieldErrors` map) - `NotAcceptableError` (406): request body unparseable - `ScopeMismatchError` (400): LiveTable scope mismatch - `ServerError` (500): safe wrapper for internal errors Errors thrown from an `@rpc` surface in the browser as the same class. Catch with `instanceof` in a `try`/`catch` around the call site. `validate()` does not exist. Throw `ValidationError` directly. Full topic: `elements man rpc`. ### Session The current user's session. Available everywhere on the server and in the browser. Read methods are reactive in the browser; a template that reads `session.isLoggedIn()` updates when the user logs in or out. ```typescript import { session } from "@elements/app"; session.login({ userId, userName }); // server-only session.logout(); // server-only session.isLoggedIn(); // boolean. reactive in browser. session.get("userId"); // SessionData["userId"] | undefined. reactive. session.getOrThrow("userId"); // same, but throws if absent. ``` ```html ``` Session values are typed through the `SessionData` interface. Declare the keys your app stores in `app/types/session.d.ts`: ```typescript declare module "@elements/app" { interface SessionData { userId: string; userName: string; role: "admin" | "user"; } } export {}; ``` `session.get(key)` returns `SessionData[key] | undefined`; `getOrThrow(key)` throws when the key is absent. There is no `getUserId()` / `getUserName()`; read `session.get("userId")`. A logged-out visitor has no session at all (no row, no token, no cookie), so identity is login state, not an anonymous id. Full topic: `elements man session`. ### Sync-style and Async Elements primitives (`sql`, `tx`, `@rpc`, `Channel`, `LiveTable`, `session`) are written sync-style. The compiler rewrites each call to its async variant (`sql` → `await sqlAsync`) and propagates `async`/`await` up the call stack automatically, including across function-expression arguments to schedulers (`setTimeout`, `addEventListener`). For third-party libraries that don't participate (fetch, Stripe SDK, navigator.clipboard), declare your function `async` and `await` those calls directly. The build leaves your manual `async`/`await` alone. For explicit promise control (e.g. `Promise.all`), import the `xAsync` variant (`sqlAsync`, `txAsync`, etc.). ### Channels Typed pub/sub over Postgres `NOTIFY`/`LISTEN`. `LiveTable` uses `Channel` under the hood; use `Channel` directly when you want raw pub/sub without table semantics (push notifications, presence, side-channel events). ```typescript import { Channel, session } from "@elements/app"; interface Alert { userId: string; level: "info" | "warning"; text: string; } const alerts = new Channel("alerts"); export default function route(req: Request, res: Response) { return new html({ alerts: alerts.listen({ filter: (a) => a.userId === session.getOrThrow("userId") }), }); } /** @rpc */ export function postAlert(text: string) { alerts.notify({ userId: session.getOrThrow("userId"), level: "info", text }); } ``` `listen()` returns a subscribed listener. Pass it to a template or return it from an `@rpc`; the browser receives a live, subscribed handle. Full topic: `elements man channel`. ### Routes Url patterns are declared explicitly in `index.ts`, not file-based. Folder names under `app/pages/` are labels. ```typescript import { App } from "@elements/app"; import config from "#config"; import home from "#app/pages/home"; import roomsShow from "#app/pages/rooms-show"; import apiRouter from "#app/api"; const app = new App(); app.route("/", home); app.route("/rooms/:id", roomsShow); app.route("/api", apiRouter); // mount a sub-router app.route("/api/rooms", () => sql(`select id, name from rooms`)); // data route app.start(config); ``` URL patterns use the URLPattern standard. Dynamic segments: `:name`. A page route returns `new html({...})`. A data route returns serializable data (Elements json), or writes to `res` directly with `res.write` / `res.end`. `AppError` subclasses work the same in routes and `@rpc`. A `LiveTable` or listener returned from a route or rpc arrives in the browser as a live, subscribed handle. Fetch initial page data inside the route, not from the browser via `@rpc` after the page loads. Full topic: `elements man router`. ### Jobs and Cron ```typescript // app/jobs/send-welcome.ts import { Job, email } from "@elements/app"; import { WelcomeEmail } from "#app/emails/welcome"; /** @job */ export class SendWelcomeJob extends Job<{ to: string }> { static maxAttempts = 5; run() { email({ to: this.fields.to, subject: "welcome", body: new WelcomeEmail() }); } } ``` ```typescript // index.ts app.cron("every 5m", "rank posts", () => new RankPostsJob().schedule()); app.cron("every day at 2am", "archive", () => new ArchiveOldRowsJob().schedule()); ``` `.schedule()` inside `tx()` joins the surrounding transaction. The job becomes visible only after commit. The project server runs the worker; jobs are stored in Postgres. One machine runs each cron tick (Postgres-arbitrated leader election). Keep cron callbacks small; enqueue and let the worker do the work. Use `/** @job */` and suffix the class name with `Job`. Full topic: `elements man jobs`. ### Tests Tests run as part of every build; failures show up alongside compile errors. Each `test()` body runs inside a Postgres transaction that rolls back at the end, so writes never pollute the test database. ```typescript // app/lib/users.test.ts import { test, assert, equal, sql } from "@elements/app"; test("users", () => { test("create", () => { let user = sql(`insert into users (name) values ('alice') returning *`).firstOrThrow("insert returned no row"); equal(user.name, "alice"); }); test("does not see the previous test's row", () => { let count = sql<{ n: number }>(`select count(*) as n from users`).firstOrThrow(); equal(count.n, 0); }); }); ``` Files: `test.ts` or `.test.ts`, anywhere in the tree. Run `elements test -json` to query state. `test()` calls nest for grouping. Assertions: `assert`, `equal`, `errorf`, `fatalf`. Full topic: `elements man tests`. ### Config `config.jsoc` is evaluated at build time. JSOC is JSON with comments, unquoted keys, trailing commas, and an `env(...)` function. Missing required env vars or wrong types fail the build, not the deploy. ```jsoc { database: { name: "my_app", host: env("DB_HOST", "127.0.0.1"), // default when unset port: env("DB_PORT", 5433), // default when unset }, session: { expires: "30d", // sliding expiry }, } ``` Env vars come from the OS at startup. After startup, edit `config/env/.env` for hot reload. Full topic: `elements man config`. ### Css Style Every page imports `app/shared/styles/page.css`, which imports `@elements/style`, a built-in, CSS-only design system: design tokens for colors, spacing, type, and radii, a reset, finished styles for bare HTML elements, and classes for buttons, forms, cards, callouts, pills, and tabs. Before writing CSS for anything `@elements/style` already covers, read `elements man style` and use what it gives you. Bare elements are already styled. A `