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
ADocumentRoom 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:
- The complete ordered request is atomic.
- Appends for one document serialize across independent room/runtime instances; a process-local lock is insufficient.
- Parent references may resolve to bootstrap, durable history, or an earlier event in the same request, never to a later event.
- An exact
batchIdplus payload resend succeeds idempotently. Reusing an id for a different payload is a conflict. - Returned batch ids correspond to every input batch in input order and are returned only after durable commit.
- The store rechecks write authorization inside the serialized durable boundary for every append; room-level cached authorization is not a substitute.
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:
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
afterCursorin ascending durable order. - Echo the request’s
requestIdand return the page’sbatches,nextCursor, andcompletefields 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
nextCursorto its final durable row. A page withcomplete: falseis therefore non-empty;completemeans that page read observed no later durable row. - Keep public read-only repair available.
- Do not pause live fan-out while repair is active.
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 runsPresenceRoom.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 asweep()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.