Collections
A collection is super-line's persisted-state primitive: named, typed state you declare on the contract, so the server validates every write and the types flow end-to-end with no codegen. It's the relational successor to the retired last-writer-wins Store family.
One collection(n) concept, two consistency models:
- Row collections — a table of many small rows you filter, subscribe to in subsets, join, and secure per row. Last-writer-wins. Reach for this for messages, users, tasks, orders — anything tabular.
- CRDT document collections — one opaque document opened by id, whose concurrent edits merge instead of clobbering. Reach for this for a canvas, a rich-text doc, a shared config — one resource edited concurrently.
Both are declared on the contract and validated on every write; they differ only in how concurrent writes resolve.
The division of labor
super-line does one job here, and does it authoritatively:
- super-line is the server-authoritative sync source. It owns the state, validates every write against the contract schema, enforces access control, and streams each caller exactly the subset it's allowed to see — live, across reconnects and across nodes.
- The client is a query/merge engine on top. For rows, TanStack DB runs joins, live queries, and optimistic mutations in the browser over the synced rows. For documents, a CRDT engine merges opaque deltas. super-line ships no query engine of its own — it syncs; the client queries.
That split is the whole idea: row-level security and validation are enforced at the source, where they can't be bypassed, while the rich client work happens where it belongs.
Declared on the contract
Collections live in defineContract alongside your roles, as a top-level collections block. A row collection has a key; a CRDT document collection has a crdt option instead:
import { defineContract } from '@super-line/core'
import * as z from 'zod'
export const api = defineContract({
collections: {
messages: { schema: messageSchema, key: 'id' }, // LWW rows (queryable)
scenes: { schema: sceneSchema, crdt: { mode: 'document' } }, // CRDT docs (opened by id)
},
roles: { user: { clientToServer: { /* … */ } } },
})Both ends import this one definition, so row and document types flow end-to-end — RowOf<typeof api, 'messages'> is the same shape on the server handle, the client handle, and the TanStack collection. The server validates every write against the schema, restoring the end-to-end-types-plus-validate-every-message promise for persisted state.
Which one do I want?
If you'd query, filter, paginate, join, or secure per row → row collection. If one resource is edited concurrently and must merge → CRDT document collection. When in doubt, start with rows — they're the common case.
This section
| Page | What it covers |
|---|---|
| Row collections | Declare rows, subscribe to live subsets, mutate in atomic batches, the query IR, useCollection. |
| CRDT document collections | Open a doc by id, merge concurrent edits, validate-before-commit, tolerant schemas, useDoc. |
| Row-level security & policies | Deny-by-default read/write policies, the RLS filter, CRDT guards, policy staleness. |
| Querying with TanStack DB | Joins, live queries, and optimism via the @super-line/tanstack-db adapter. |
| Backends & clustering | The capability matrix, relay vs. self clustering, every factory option, advisory foreign keys. |
| Choose a collection backend | Which backend to use: durability, coordination, what you have to operate. |
New to collections? The fastest way in is Tutorial 4 · Store your data — a live, filtered row-set end to end in a few minutes.