Skip to content

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:

ts
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 rowrow collection. If one resource is edited concurrently and must mergeCRDT document collection. When in doubt, start with rows — they're the common case.

This section

PageWhat it covers
Row collectionsDeclare rows, subscribe to live subsets, mutate in atomic batches, the query IR, useCollection.
CRDT document collectionsOpen a doc by id, merge concurrent edits, validate-before-commit, tolerant schemas, useDoc.
Row-level security & policiesDeny-by-default read/write policies, the RLS filter, CRDT guards, policy staleness.
Querying with TanStack DBJoins, live queries, and optimism via the @super-line/tanstack-db adapter.
Backends & clusteringThe capability matrix, relay vs. self clustering, every factory option, advisory foreign keys.
Choose a collection backendWhich 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.

Released under the MIT License.