Skip to main content

Collaborative sets

A g-set column is the right shape when several people add elements to the same collection and nobody's contribution may be lost: concurrent writers union their elements, and every replica — including a fresh boot — converges on the same set. This is verified with two synced clients in package/schema/merge_sync_test.ts.

Declare the set

// src/schema.ts
import { type ArrayColumn, s } from "@nzip/lofi/schema";

export const app = s.defineApp({
tagged: s.table({
name: s.string(),
tags: s.array(s.string()).merge("g-set") as unknown as ArrayColumn<"TEXT">,
}),
});

Two constraints of the pinned alpha shape this declaration:

  • The cast is required. .merge() returns the untyped builder (a pinned upstream type bug); the cast restores the array column type. The runtime object is unchanged.
  • Keep the g-set table in its own single-table app. A g-set column destabilizes writes to sibling tables in the same app — the conformance canary pins the hang. When the model also needs ordinary tables, they belong in a separate app until upstream stabilizes this.

Write whole sets, converge on the union

Writers always submit their full local set; the merge takes care of the rest:

const doc = await db.insert(app.tagged, { name: "doc", tags: ["a"] }).wait({ tier: "global" });

// Two clients, offline at the same time:
alice.update(app.tagged, doc.id, { tags: ["a", "b"] });
bob.update(app.tagged, doc.id, { tags: ["a", "c"] });

// After both reconnect, every replica reads ["a", "b", "c"].

The set is grow-only: the verified behavior is that concurrent writes union. Removing an element is not part of the verified surface — if entries must be retractable, model the collection as its own table with one row per element and delete rows under an ordinary policy instead.