# Partitions One slice of a table per page: the comments for a post, the messages in a room, the tasks for a project. ```ts Route import { Request } from "@elements/app"; let comments = new LiveTable(); export default function route(req: Request) { return new html({ comments: comments.view({ postId: req.params.id }) }); } ``` The object passed to `view()` is a set of column equalities. One object does four jobs: - It decides which rows the page receives. A row reaches a page only when it matches the page's partition, so a comment on one post reaches the browsers watching that post and no others. - It narrows the select. Auto-select adds `where post_id = $1`; a custom select receives the object and writes the same clause. - It fills the columns on insert. `comments.insert({ text })` gets `postId` from the view. Passing a different value throws. - It is checked on every mutation. A row naming another partition is refused with `PartitionMismatchError`, before any handler runs and independent of anything the handler checks. Several columns compose the same way, and the order you write them does not matter: ```ts Several columns tasks.view({ projectId, status: "open", }) ``` Only a column holding a string, number, or boolean can partition. The value is a SQL parameter and a string comparison on the server and in the browser. ## Authorization lives at the call Nothing about a partition is a permission. `comments.view({ postId })` opens that post's comments for whoever asked, so the route or RPC that calls it is where the check goes, because that is the only place that knows who is asking: ```ts Route import { Request } from "@elements/app"; export default function route(req: Request) { session.isLoggedInOrThrow(); return new html({ transfers: transfers.view({ userId: session.getOrThrow("userId") }) }); } ``` `view()` is server-only, and so is the LiveTable it is called on. The browser receives the view already partitioned. There is no way for browser code to widen it, because it never holds the declaration that could. ## Every write reaches every matching page Pages on `view()`, `view({ roomId })`, `view({ ownerId })` and `view({ roomId, status })` can all be open on the same table at once. A write reaches every page whose partition the row matches, on every app server, whichever process makes it: a route, an rpc, a job, another app server, or a Postgres trigger. Every write notifies the table's one channel. Each app server listens on it once and matches each row against the views open there, server-side, before anything is sent. Views are indexed by the columns they partition on, so matching a row costs one lookup per distinct partition shape on the server, however many pages are open. A browser receives only rows that match its view's partition. ## A row that changes partition When an update moves a row from one partition to another, `status` going from `"open"` to `"closed"` say, the pages on the partition it left receive a delete and the pages on the one it joined receive an insert. Each app server tracks which rows each page holds, so the writer does nothing extra, and a write from a trigger moves rows the same way. The delete carries the row's id and nothing of its new values. Any column may be a partition key, and a view stays correct when rows move. ## Filtering beyond the partition Everything narrower than a column equality is `filter()` in the template: ```ehtml Template
  • !c.archived)}> ``` A custom `select` may also filter in SQL to keep the first render small. The stream follows the partition: a row that matches the partition reaches the browser whether or not the select would have returned it. Keep the template's filter as the rule and treat the select's as an optimization of the snapshot. For a table where the unwanted rows outnumber the wanted ones, use a window (see `windows`) or a narrower partition.