Skip to main content

Collaboration Runtime Contract

@softmaple/collab-runtime is the server-side semantic boundary between the versioned collaboration protocol and concrete hosting infrastructure. It lets the same room behavior be implemented on the current Nitro/Redis/Postgres stack and on a future runtime without importing either environment into the runtime package. The package supplies the shared DocumentRoom and PresenceRoom implementations as well as their contracts. The production Nitro host in apps/collab-nitro owns only transport and infrastructure adapters: raw WebSocket ingress, Supabase authorization, Prisma/Postgres history, and Redis-backed fan-out and connection leases. PresenceRoom is documented separately below — it shares no capability instance with DocumentRoom.

Capability boundary

PresenceRoom shares no capability instance with DocumentRoom: separate PresenceStore/PresenceFanout from RoomFanout, and its own PresenceSessionHooks. Only ConnectionLimiter is reused as-is, scoped by a different room id, so a presence failure structurally cannot block durable document convergence. The room consumes parsed ClientCollabMessage values and sends existing ServerCollabMessage values. DocumentEventPage is derived from RepairResponseMessage, so this layer cannot silently invent a second wire shape.

Ownership and lifecycle

A DocumentRoom represents one document. Its peer set is local to one room instance; durable history, cross-instance fan-out, and global connection admission arrive through shared capabilities. Two server instances may own different local peer sets for the same document and communicate only through those capabilities. Document identity is exact and immutable. An Auth message must match DocumentRoom.documentId before the authorization hook runs. Sessions, event-store calls, and fan-out subscriptions use that same id, and a room must discard a fan-out event for any other document. This is the cross-document isolation boundary for path-routed rooms and Durable Objects alike. join registers a transport peer. Authentication resolves a normalized access record, and the room constructs the session using the validated document id, session id, and protocol version from the Auth message. After authentication, that session protocol version is authoritative for outbound messages; later messages do not renegotiate it. Authenticated sessions have an actor, role, and write flag. Public sessions are protocol v3 only, anonymous, and always read-only. Authorization refresh may update role/write access, but an access-mode or actor change revokes the session instead of silently changing its identity. Connection admission happens before Ready. A partially completed join must release every acquired subscription and lease. leave and close must be safe after partial setup and must release resources even when authorization has already been invalidated. A lost lease or revoked authorization closes the peer. Operations for one peer may arrive concurrently. The room synchronizes their state transitions so receive/leave/close races cannot create two sessions or leak partial resources, while an Auth observed during pending authentication is still rejected. The current compatibility policy is 100 physical connections per document, a 45-second lease TTL, and 15-second lease/authorization refresh. A DocumentRoomPolicy supplies the connection values and authorization cadence; DEFAULT_DOCUMENT_ROOM_POLICY records this compatibility baseline. Duplicate admission refers to the server-generated physical peer id; the client session id is correlation metadata and may be reused on reconnect. Origin validation, raw message byte limits, JSON parsing, and other transport-measured policies remain host adapter responsibilities. Protocol validation remains in @softmaple/collab-protocol. The adapter preserves the current 256 KiB limit (close 1009), 120 messages per 10 seconds limit (InvalidMessage, retryable, then close 1013), and malformed-message behavior (InvalidMessage, non-retryable). Browser clients close on protocol Error and reconnect only when it is retryable.

Durable append and acknowledgement

DocumentEventStore.append(documentId, actorId, batches) has these requirements:
  1. The complete ordered request is atomic.
  2. Appends for one document serialize across independent room/runtime instances; a process-local lock is insufficient.
  3. Parent references may resolve to bootstrap, durable history, or an earlier event in the same request, never to a later event.
  4. An exact batchId plus payload resend succeeds idempotently. Reusing an id for a different payload is a conflict.
  5. Returned batch ids correspond to every input batch in input order and are returned only after durable commit.
  6. The store rechecks write authorization inside the serialized durable boundary for every append; room-level cached authorization is not a substitute.
Store adapters translate infrastructure-specific failures into the runtime taxonomy before they reach DocumentRoom: DocumentEventConflictError.details.conflictType uses the same four conflict categories defined in the consistency document. A repair read can reject only with DocumentEventStoreUnavailableError. For a client Event message, the room ordering is:
An append failure produces neither acknowledgement nor fan-out. An acknowledgement means durable commit, not delivery to every replica. Fan-out is best-effort after commit: a publish failure does not roll back the write or invalidate the acknowledgement, and peers recover through repair. If the origin disconnects before receiving its acknowledgement, an exact resend is idempotent. Fan-out contains document Event batches only. Ready, DurableAck, repair responses, and errors are direct peer messages. Delivery includes the writer and may be duplicated, delayed, lost, or reordered across publishers; consumers deduplicate by batch id. A failed send to one local peer must not prevent delivery to the remaining peers. RoomFanout.publish owns loopback as well as cross-instance delivery; the room does not need a second local broadcast path.

Repair and resync

Repair is client-requested durable history synchronization:
  • Start at cursor "0". Every cursor is a non-negative decimal string.
  • Read batches strictly after afterCursor in ascending durable order.
  • Echo the request’s requestId and return the page’s batches, nextCursor, and complete fields only to that peer.
  • Keep every page within DOCUMENT_EVENT_PAGE_LIMIT (currently 100 batches).
  • An empty page preserves its input cursor. Every non-empty page strictly advances nextCursor to its final durable row. A page with complete: false is therefore non-empty; complete means that page read observed no later durable row.
  • Keep public read-only repair available.
  • Do not pause live fan-out while repair is active.
Repair pages are not a frozen snapshot. A committed live Event can arrive before, after, or between pages and may duplicate a batch in a page. The client’s batch-id deduplication makes every supported interleaving converge. The server does not maintain a hidden repair cursor or trigger blind repair on every conflict. On reconnect, the browser repairs to completion before it exact-resends its pending write. That one-in-flight queue is a client invariant that the room must remain compatible with; it is not server-owned session state.

Presence room

PresenceRoom is a separate, payload-opaque state machine beside DocumentRoom. It shares no capability instance with the document room — PresenceStore/PresenceFanout/PresenceSessionHooks are distinct from DocumentEventStore/RoomFanout/DocumentSessionHooks — so a presence failure structurally cannot block durable document convergence, and a document-store outage cannot block presence. Presence is authenticated-only; there is no public-access path. The room never reads a presence payload directly. A PresenceCodec owns every wire type string, envelope/patch/auth/heartbeat shape, and the published updates object; the room only reads the three identity fields it owns on a stored member (clock, connectionId, userId) and dispatches on the codec’s classification of an inbound frame. This is what lets the same PresenceRoom run behind @softmaple/awareness/protocol in production and behind a dependency-free test codec in this package’s own tests.

Room-owned behavior

Origin validation, ?roomId= parsing, and the frame byte-size limit remain host adapter responsibilities, exactly as they do for DocumentRoom. Byte measurement is transport-specific.

Refresh modes

PresenceRoomOptions.refreshMode mirrors DocumentRoomRefreshMode:
  • background — one per-peer heartbeat-expiry timer, unref’d so it never blocks process exit. Used by hosts that do not hibernate (Nitro).
  • on-message — no timers. Every inbound frame first runs PresenceRoom.sweep(now) under a room-level maintenance lock, which closes peers past their heartbeat deadline and drains lapsed store members into Leave broadcasts. Used by hosts that hibernate (a Durable Object), where a sweep() call can also be driven by an alarm instead of an inbound frame.
sweep(now?) is public on PresenceRoom precisely so a host’s alarm or timer can drive liveness without reimplementing expiry rules, and so the shared conformance suite in @softmaple/collab-runtime/testing exercises it identically on both hosts. It is idempotent: a member already purged by an earlier sweep() or read is not reported expired again. PresenceRoom.resume(peer, state) restores an already-authenticated session after a hibernating host wakes a peer, without a new wire Auth or a second auth-ok — it revalidates through PresenceSessionHooks.refresh first, the same way DocumentRoom.resume does.

Not a convergence layer

DocumentRoom coordinates validated dispatch, authorization, durable persistence, acknowledgement, repair, fan-out, connection policy, and lifecycle. It never integrates rich-text operations or decides convergence. EG-walker and @softmaple/block-model remain the only convergence/model layers, and awareness remains an independent ephemeral channel. The conflict categories and retry behavior that every runtime must preserve are defined in collaboration-consistency.md.