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 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 ​

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.