Share a table
For a table that more than one copy writes — a passable or session document. Decide the shape first (Choose a shape); this page is what follows once you know a table is shared. The worked example is the receipts application in Examples.
Mark it, and take out what replication owns
Put -- dai:replicated directly above the table, and remove any PRIMARY KEY, AUTOINCREMENT, UNIQUE and CHECK from it. Keep everything about one copy — a setting, a draft, which item is showing — in ordinary local tables beside it.
SHARED-MARKER
Mark a shared table
Put the line -- dai:replicated directly above each table that more than one copy writes, with nothing but whitespace between the comment and CREATE TABLE. Only tables every party must agree on get it; everything about one copy stays local (SHARED-LOCAL-STAYS-LOCAL).
Why. The compiler rewrites a marked table into an append-only one with the columns, key, triggers and views replication needs. A marker further up is an ordinary comment and declares nothing.
Applies to passable, session · refused at build · SHARED-MARKER in Constraints
SHARED-NO-KEY
No PRIMARY KEY, no AUTOINCREMENT
A replicated table declares no PRIMARY KEY and no AUTOINCREMENT. Its identity is the entity the write surface returns (SHARED-ENTITY-IDENTITY).
Why. The key belongs to replication. Two copies both advancing one counter allocate the same ids for different rows.
Applies to passable, session · refused at build · SHARED-NO-KEY in Constraints
SHARED-NO-UNIQUE-CHECK
No UNIQUE, no CHECK
A replicated table declares no UNIQUE and no CHECK constraint, on a column or on the table.
Why. A UNIQUE(game_id, ply) refuses exactly the rows a merge exists to surface: two people acting at the same point is a conflict to show a person, not an error to raise at them. A CHECK that differs between two versions of the application rejects the other copy's honest rows, and they arrive as rejected rows rather than a refused merge, so nobody can see what happened.
Applies to passable, session · checked by the lint (shared-table-constraint) · SHARED-NO-UNIQUE-CHECK in Constraints
SHARED-LOCAL-STAYS-LOCAL
What belongs to one copy stays local
Keep in ordinary local tables everything about this copy rather than the document: settings, drafts, which item the screen is showing, what this person has hidden, the name this person goes by. Local tables are never merged, so they may use PRIMARY KEY, UNIQUE and CHECK freely. They travel only in a whole-document copy — a file, or the host menu's share — where a person opening it for the first time starts from the sender's local rows. An invite into one session carries none of them, and a copy that already exists keeps its own local rows when another copy's shared rows are merged into it.
Why. A setting in a shared table changes the other person's screen, and a draft in one is sent before it is finished.
Applies to passable, session · not checked by anything · SHARED-LOCAL-STAYS-LOCAL in Constraints
Write through the write surface
SHARED-WRITE-SURFACE
Write shared rows only through window.dai.replicated
Write to a replicated table only with window.dai.replicated.insert(table, values), .change(table, entity, values) and .remove(table, entity), after await window.dai.openDatabase(). values is an object of your own columns. change takes every one of your columns, not only the ones that changed. Never run INSERT, UPDATE or DELETE against a replicated table — not in JavaScript, not in a kit control, not in a seed block.
Why. Rows are appended and never changed in place: an UPDATE or DELETE is refused with REPLICATED_TABLE_IMMUTABLE, and a raw INSERT fails because it lacks the replication columns only the write surface fills in.
Applies to passable, session · refused at run time; checked by the lint (shared-raw-write) · SHARED-WRITE-SURFACE in Constraints
SHARED-ENTITY-IDENTITY
A shared row's identity is its entity
Refer to a shared row by the entity the write surface returned: 32 lowercase hex characters. Read it back as lower(hex(_r_entity)). To point one shared row at another (a move at its game), store that hex string in an ordinary TEXT column and compare it with lower(hex(_r_entity)). A particular version of a row is its key, (_r_replica, _r_seq) — the same on every copy — so an application that needs to name "this exact wording" (what a person accepted, say) can use it.
Why. The entity is the same on every copy, where an id of your own would be allocated separately on each.
Applies to passable, session · not checked by anything · SHARED-ENTITY-IDENTITY in Constraints
SHARED-KIT-READS
The kit reads shared tables; it does not write them
The kit's write controls — data-run, <dai-form run=…>, <dai-attach run=…> — run plain SQL, so they are for local tables only. On a shared table they fail (SHARED-WRITE-SURFACE), and the kit neither catches the error nor shows it. The kit's reading elements, <dai-rows> and <dai-value>, work over t_current views; redraw them on a merge with window.daiKit.refresh() (SHARED-REDRAW-ON-MERGE). Write shared rows in JavaScript through window.dai.replicated.
Why. Run against the rewrite: a kit INSERT into a replicated table fails with SQLite's NOT NULL error, and an UPDATE or DELETE with REPLICATED_TABLE_IMMUTABLE — uncaught, so the person sees nothing happen.
Applies to passable, session · checked by the lint (shared-raw-write) · SHARED-KIT-READS in Constraints
Read from the view, and redraw when rows arrive
SHARED-READ-CURRENT
Read shared rows from the _current view
Read a replicated table t only through the view t_current, which holds one row per live entity. Never SELECT from t itself for display or logic. Use t_conflicts or t_heads only to show or resolve a conflict (SHARED-SURFACE-CONFLICTS).
Why. The base table holds every version of every row: superseded edits, tombstones of deleted rows, and — in a session — rows from non-members and rows written after the close. Reading it shows all of them at once.
Applies to passable, session · checked by the lint (shared-base-read) · SHARED-READ-CURRENT in Constraints
SHARED-REDRAW-ON-MERGE
Redraw when the other copy's rows arrive
Listen for the dai:merged event on window and redraw everything drawn from shared tables when it fires: window.addEventListener("dai:merged", (event) => { redraw(); }). event.detail carries applied, duplicate, rejected, newReplicas, conflicts and via — "carrier" when a file or link was opened, "mailbox" when rows arrived in the background. If the page uses the kit's reading elements, call window.daiKit.refresh() in the listener. A redraw must never discard what the person is in the middle of — text typed into a field, an editor that is open, a selection: rows arrive whenever the other copy's changes do, including mid-sentence. Keep work in progress outside what the redraw rebuilds — in a form written once in the HTML rather than recreated on every draw, or in a local drafts table the redraw reads back — or leave the element being edited untouched until it is saved or cancelled. The same failure arriving at start-up rather than mid-edit is NO-INPUT-LOST-WHILE-OPENING.
Why. Nothing else tells the application that another copy's rows landed. Without it the application draws once and redraws only after its own writes, so a two-person document looks broken in exactly the case it exists for. And a redraw that rebuilds an open editor from the stored wording throws away what was being typed, silently — found by running a blind candidate over the mailbox, where a background merge landed while a term was being edited.
Applies to passable, session · checked by the lint (shared-no-merge-listener) · SHARED-REDRAW-ON-MERGE in Constraints
Store facts, derive the rest
SHARED-NO-DERIVED-STATE
Store facts, derive everything else
Store in a shared table only the facts people enter or acts they take — a receipt, a move, a mark. Never store anything computable from them: no board, no score, no turn, no total, no balance, no "last updated", no status that follows from other rows. Compute it from the rows each time it is drawn, preferably in SQL.
Why. A stored total is a second opinion about what the rows say, and after a merge it is wrong: each copy computed it from the rows it had, and neither computed it from the union.
Applies to passable, session · not checked by anything · SHARED-NO-DERIVED-STATE in Constraints
Check it
npx dai check ./your-appThe lint refuses a raw write to a shared table, a read of the table itself, a missing dai:merged listener, an application that edits shared rows and never shows a conflict, and UNIQUE or CHECK on a shared table. Each finding names the constraint it enforces.