Run a session
For a closed group admitted by invite. Everything in Share a table applies too; this page is what a session adds. The worked example is Your first two-player app.
Declare it
SESSION-PROFILE
Declare the session profile
Declare a session document with one line comment in schema.sql: -- dai:profile session max_parties=N close=any|creator. N is the most people the document allows, at least 1 — but today a session seats two whatever N says: session.create() mints the creator's seat and one open seat, and no call adds another (backlog D6). Declare max_parties=2, and do not build an application that needs a third member. close=any lets any member close a session; close=creator lets only the person who created it; it defaults to any. The document must also have at least one table marked -- dai:replicated. Every replicated table then carries the session of each row.
Why. The profile is signed into the document, so the size of the group is the creator's stated limit rather than something the application decides. A malformed profile, or one with no replicated table, is refused at build.
Applies to session · refused at build · SESSION-PROFILE in Constraints
Start one
SESSION-CREATE
A new game is a new session
Start each game, match or agreement with const { session, seat } = window.dai.replicated.session.create(). It seats the creator and leaves one open seat for the invitee; session is the id to keep (hex), seat is the open seat. Then insert the thing itself — the games row — with that session (SESSION-ROW-CARRIES-SESSION). Do both in one transaction if you write local rows beside them: session.create() and insert work inside a BEGIN … COMMIT you open. The creator is a member from the moment the session exists, so the creator's rows are admitted before anyone has joined — the first move can be made before the invite is sent.
Why. A session is the unit of membership. Rows written outside one belong to nobody, and a second game in the same session would share the first game's roster.
Applies to session · not checked by anything · SESSION-CREATE in Constraints
SESSION-ROW-CARRIES-SESSION
Every insert names its session
In a session document, pass the session id as the third argument of every insert: window.dai.replicated.insert("moves", values, session). change and remove take no session — they inherit the entity's.
Why. Every replicated row in a session document belongs to a session. Run against the write rules: an insert without one throws "A row for … carries no session, but <table> declares the session profile", and nothing is written.
Applies to session · refused at run time · SESSION-ROW-CARRIES-SESSION in Constraints
Invite
SESSION-INVITE
An invite is a shared link
Invite the other party by asking the host to share, naming the session: a button that calls window.dai.requestShare(session). There is no invite call of your own. The copy that travels holds only that session's rows — none of the document's other sessions, and none of this copy's local tables — so the recipient gets this one game and nothing else of the sender's. Without a session, requestShare() offers the whole document, every session in it, as the host's own menu does; use it for that, never for an invite. After a copy has been shared by link, the host moves new rows between the copies on its own, and they arrive as dai:merged with via "mailbox"; a copy handed over as a file carries its rows when it is opened. The application never sends rows itself; a "send" button that calls requestShare() again is only needed where copies travel as files.
Why. The host mints the key that lets the two copies exchange rows and makes the link; the application only asks, and only the application knows which game it is inviting to. Filtering to that game is what keeps a person's other games — and whatever they keep only on their own device — out of every invite they send.
Applies to session · not checked by anything · SESSION-INVITE in Constraints
Join on open
SESSION-JOIN-ON-OPEN
Take the open seat when an invite is opened
When this copy opens an invite, bind its open seat with window.dai.replicated.session.join(session, seat): once at start-up, and again in the dai:merged listener only when event.detail.via === "carrier" — never for "mailbox". Join only if this copy is not already a member and an open seat exists: the open seat is a _dai_seat_current row for the session whose seat no _dai_binding_current row binds. Join the session the invite was sent for. An invite carries only that session and none of the sender's local rows (SESSION-INVITE), so it is a session with an open seat that this copy did not create and is not a member of — in a fresh copy made from an invite there is exactly one. That includes a copy whose seat was contested or replaced: opening the creator's fresh invite is how it gets back in, and excluding copies that were ever seated would lock it out for good. Prefer the item that is showing when it is joinable (a copy that arrived as a whole document carries the sender's local rows, including which item was showing), otherwise take the newest joinable one, and make it the item showing.
Why. Membership comes from opening an invite, not from rows arriving. A copy that joined on every background merge would re-take a seat it had lost, and a copy that joined twice would contest its own seat.
Applies to session · not checked by anything · SESSION-JOIN-ON-OPEN in Constraints
Know who is in
SESSION-MEMBERSHIP
Read membership, and show the three ways to be outside
This copy's replica id is SELECT lower(hex(id)) AS id FROM _dai_replica. It is a member of a session when _dai_member has a row for (session, replica). Enable writing only for members, and show a copy that is not one which of three states it is in: it holds the rows but never joined (it was forwarded the document, not invited); it joined but its seat was contested or replaced (SESSION-CONTESTED-SEAT); or the session is closed (SESSION-CLOSE). The _current views of a session document show only admitted rows — rows by members, written before any close.
Why. A non-member's rows are kept but never admitted, so an application that let a non-member play would show them their own moves and nobody else ever would. Saying which state a copy is in is the difference between a message and a hang.
Applies to session · not checked by anything · SESSION-MEMBERSHIP in Constraints
Repair a contested seat
SESSION-CONTESTED-SEAT
A contested seat is a state to show
A seat bound by two or more different replicas is contested — two people opened the same invite — and admits neither. Detect it as a _dai_binding_current seat with count(DISTINCT _r_replica) > 1 for the session. Show the creator that the invite went to more than one device and offer a fresh invite: window.dai.replicated.session.reseat(session), then share again. Show a copy whose own seat was lost that nothing it did lost its place, and that the creator can send a new invite. reseat refuses with NOT_SEAT_CREATOR for anyone but the creator and with CANNOT_RESEAT when no seat is contested.
Why. It is resolved without a clock deciding who opened the invite first, so neither copy can be admitted until the creator repairs it; an application that treated it as an error would leave both people stuck.
Applies to session · refused at run time; not checked by anything · SESSION-CONTESTED-SEAT in Constraints
Close
SESSION-CLOSE
Closing is separate from finishing
Ending the activity is an ordinary row: a resignation, a final mark, a signature. Closing the session is a separate, heavier act — window.dai.replicated.session.close(session) — after which rows written later than what the closer had seen are not admitted. Offer it only on a finished session, never as the way to end a live one. Closing as part of an act whose point is finality — sealing an agreement once both have accepted it — is exactly what close is for: write the act as a row, then close. Read whether a session is closed from _dai_close_current (any row for the session). Under close=creator a non-creator's close is refused with CLOSE_NOT_PERMITTED; hide or disable the control for them.
Why. A close is final for the group, and it is decided by what the closer had seen rather than by a clock. Folding it into "resign" would end a session the other person had not finished with.
Applies to session · refused at run time; not checked by anything · SESSION-CLOSE in Constraints
The views these read — _dai_member, _dai_seat_current, _dai_binding_current, _dai_close_current — are listed in Schema, and why seats work this way is in Seats and contested seats.