# RPC and LiveTable in Templates Calling an `@rpc` from a template and rendering a LiveTable, the two places html meets the rest of the framework. ## RPC + HTML Mark a function `@rpc` to call it from the browser. The compiler wires the network call. ```ts /** @rpc */ function saveUser(form: Form): User { return sql(`insert into users (name, age) values (${form.name}, ${form.age}) returning *`).firstOrThrow("insert returned no row"); } ``` Call it like a normal function from a handler. ```html
saveUser(form)}>
``` Elements primitives (`sql`, `@rpc`, `tx`, `session`, `Channel`, `LiveTable`) are sync-style: you write them without `async`/`await`. The compiler converts them to async at build time and propagates the conversion up the call stack. If you want the async version directly, import the `xAsync` variant (`sqlAsync`, `txAsync`, `insertAsync`). ```ts let user = saveUser(form); let users = sql(`select * from users`); ``` When you genuinely need explicit async (third-party libraries like the Stripe SDK, `fetch`, `navigator.clipboard`), declare the function `async` and `await` those specific calls. The build leaves manual `async`/`await` alone. ## LiveTable + HTML LiveTable provides crud and realtime for any reactive data source. The source can be a database table, a view, or anything else you can read and write. By default a LiveTable binds to a database table, but you can override the crud handlers to back it with whatever you want. ```ts let comments = new LiveTable(); ``` This binds to the `comments` db table (variable name → table name, camelCase to snake_case). Realtime is on by default, so every page that uses `comments` gets cross-client updates. Pass `realtime: false` for a snapshot-only read. In the template, iterate with `e:for`: ```html
  • {comment.text}
``` Mutate from event handlers. `insert`, `update`, and `delete` take an optional `resetUI` callback whose only job is to clear local UI state (form fields, drafts, edit toggles) after the optimistic write. Use it to reset UI, nothing else. Put navigation like `redirect()` in the handler flow, not in `resetUI`: ```ts comments.insert({ text: form.text }, () => form.text = ""); comments.update({ ...comment, text: editText }, () => editing = false); comments.delete(comment); ``` All three are sync-style with optimistic UI. The browser updates immediately. The server reconciles afterward. On failure, the optimistic change rolls back. `e:for` over a LiveTable does O(1) updates per notify message. No re-render of the full list. **Optimistic inserts must include every field the template displays.** The optimistic row holds only the object you pass. If the template shows `comment.author`, `insert({ text })` will render `undefined` for `author` until the server reconciles. Include displayed fields at insert time even if the server canonicalizes them. Server-generated columns the template reads count too: a displayed `createdAt` (`default now()`) is `undefined` on the optimistic row, so `new Date(undefined)` renders `Invalid Date` until the broadcast lands. Pass a client-side placeholder like `createdAt: new Date()`. Full LiveTable details (scopes, custom select, custom handlers, realtime modes): `livetable`.