# 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. ```ehtml
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 to table name, camelCase to snake_case). Every page that renders a view of `comments` gets cross-client updates. In the template, iterate with `e:for`: ```ehtml
  • {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 LiveView patches only the rows that changed. A row arriving over the wire is one DOM insertion; an edit to a row repaints the bindings that read the edited field and nothing else. **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 (partitions, windows, custom select, custom handlers): `livetable`.