# Options The declaration's options, and what `view()` takes. ```ts new LiveTable({ table: "comments", // db table for auto-SQL; inferred from the variable name channel: (p) => `comments:${p}`, // override the pub/sub channel name fields: ["text", "author", { name: "createdAt", readonly: true }], select: (partition, w) => sql(`...`), // custom SELECT insert: (item) => { ... }, // custom insert, or omit for auto-SQL update: (item) => { ... }, delete: (item) => { ... }, }); table.view(); // the whole table table.view({ roomId }); // one partition table.view({ roomId }, { orderBy: "createdAt desc", limit: 50 }); // one page of it ``` A LiveTable is always live. There is no snapshot mode: a page that wants rows once, with no subscription, calls an `@rpc` that returns them from `sql`, and `e:for` iterates the array it gets back. ## Table `table` names the database table auto-SQL reads and writes. The compiler fills it in from the variable name (camelCase to snake_case), so `let blogPosts = new LiveTable()` reads `blog_posts`. Set it when the two differ. Name the variable after the table and there is nothing to set. A mismatch compiles clean and the build stays green, because nothing reads the table until a request runs: the page is a 500 saying the relation does not exist. ## Fields ```ts fields: ["text", "author", { name: "createdAt", readonly: true }] ``` If omitted, fields are introspected from the database. `id` is always implicit and readonly, and so are `createdAt` and `updatedAt`: the database fills them in, so the `createdAt: new Date()` an optimistic insert carries as a placeholder is never written. A `readonly` field is excluded from auto-insert and auto-update. Declare `fields` yourself to write one of them. camelCase in TypeScript maps to snake_case in the database (`createdAt` to `created_at`). ## Ids Every row has a string `id`, and the browser mints it, as a UUIDv7, before an optimistic insert is sent. That is what lets the broadcast echo find the row it belongs to. A table whose ids are assigned by the database (a serial column) is written through an `@rpc`, not a LiveTable mutator. ## Channel Name Every LiveTable gets a channel name automatically: `` for the whole table and `
:` for a partition, where the partition key is the view's column equalities encoded as `roomId=42`. File-path disambiguation keeps two declarations on the same database table from cross-broadcasting. Set `channel` when something outside the LiveTable publishes on the channel: a Postgres trigger, another service, or a second LiveTable that should share the broadcast. The function receives the encoded partition key, or `""` for the whole table. ```ts new LiveTable({ channel: (partition) => partition ? `comments_shared:${partition}` : "comments_shared", }); ``` A trigger that notifies the channel with `{ op, data }` works alongside the app's own broadcasts. The browser store dedupes an insert by `id`, and an update or delete is idempotent, so hearing about a write twice changes nothing. See `channel` for the trigger pattern. ## Sort There is no sort option. The initial select returns rows in database order and the template orders them: ```ehtml
  • +a.createdAt - +b.createdAt)}> ``` `sort()` returns a plain array, sorted, and the loop is live because it iterated the view to build it. A windowed view is the exception: its `orderBy` is the order the rows are held in, because a page has to be a page of something. See `windows`. ## Listen Before Select `view()` opens the Postgres `LISTEN` and awaits its commit before running the select. A `NOTIFY` fired between the two is queued on the listener and flushed when the browser attaches, and the browser dedupes it against the snapshot by `id`. You never call `listen()` yourself. For a one-shot query merged with a raw channel, see `channel`. ## Returning a view from an RPC A route handler that opens a view and passes it to its template is the common path. An `@rpc` can return a view too, for a table that depends on a runtime decision: a permission check, a parameter, a different table per call. ```ts /** @rpc */ export function watchRoom(roomId: string) { if (sql(`select 1 from roomMembers where roomId = ${roomId} and userId = ${session.getOrThrow("userId")}`).empty()) { throw new AuthError(); } return comments.view({ roomId }); } ``` ```ts let comments = watchRoom(roomId); // browser: a LiveView, sync-style ``` ## Errors - `AuthError`, `ForbiddenError`: thrown by a handler that checked the session. - `ValidationError`: thrown by a handler on bad input, or by the framework when a custom select ignores the window it was given. - `PartitionMismatchError`: a row's partition columns did not match the view's partition. Fires before any handler runs. ## Related - `database`: `sql()`, `tx()`, and the camelCase-to-snake_case boundary. - `channel`: the pub/sub layer underneath LiveTable. - `html/events`: `oninit`, `oninsert`, and `onremove` on the rows a LiveTable renders. A LiveTable has no lifecycle handlers of its own; the rows do. - `html/integration`: rendering a LiveTable from a template.