A3S Boot collaboration server
The repository includes a complete runnable backend under
examples/collaboration-server.
It is not pseudocode: its Rust integration test opens two authenticated
WebSocket clients, persists browser-generated Yjs updates through Yrs, checks
room broadcast, creates a durable Document selection comment and an attributed
text suggestion, and proves that review-only, suggestion-only, or view-only
clients cannot forge canonical content. The same durable path accepts bounded
character- and paragraph-formatting revisions from edit clients, preserves
them across Yrs restart, and protects them from suggest clients.
The example combines a3s-boot 0.2 and the native a3s-office collaboration
store:
Run the service
From the Office repository root:
The checked-in
collaboration-server.acl
listens on 127.0.0.1:8787, stores replicas under ./data/collaboration, and
permits the local Playground origins. Secrets are resolved with ACL env(...)
calls and are never stored in the file.
Pass a deployment ACL path as the first argument:
Issue a room ticket
The host backend calls the protected ticket endpoint only after its normal user authentication and document authorization have succeeded:
The standard API response contains data.webSocketUrl. That URL includes a
short-lived, room-scoped ticket and is the only credential passed to the
browser. Never expose A3S_OFFICE_ADMIN_TOKEN in frontend code.
Attach the browser client
The complete
client.ts
implements OfficeCollaborationTransport, bounded base64 wire encoding,
Awareness relay, stale-peer cleanup, exponential reconnect, and a fresh
two-way state-vector handshake after reconnect.
Pass session and presence to any React, Vue, or Web Component editor. On
unmount, destroy room, presence, session, awareness, and the host-owned
Y.Doc in that order.
The signed ticket uses schema version 2. Its actorName must equal the local
session actor name and the author of every new comment, reply, or Document
suggestion. The adapter verifies artifact ID/kind, namespace, actor
ID/name/kind, mode, and server Yjs client identity in collaboration.ready; a
mismatch closes the connection instead of attaching the wrong room identity.
Update authorization
For a Document comment update, the service holds the durable store lock,
clones the committed Yrs state, applies the submitted update to the candidate,
and compares their semantics. Only document.comments, retained/append-only
comment order and immutable claims, commentsPresent, and
documentComment marks may change. Canonical text or structure, other roots or
options, forged author/actor values, reordered existing records, invalid
anchors, changed foreign records, and deletion of another actor's record fail
with FORBIDDEN. The candidate is persisted and broadcast only after this
check succeeds, so authorization never depends on trusting caller-authored Yjs
bytes or origin metadata.
For a Document suggest update, the service uses the same lock and candidate
state boundary. It removes suggestion effects from the committed and candidate
ProseMirror trees and requires the resulting canonical text, structure, and
formatting to match. Every non-content root must also remain byte-semantically
equivalent. A new insertion or deletion mark must carry a stable ID, canonical
UTC date, and the exact actor ID/display name authenticated by the ticket. The
actor may safely extend or withdraw its own insertion proposals, but cannot
rewrite another actor's proposal or the canonical text targeted by a deletion.
A replacement is authorized as one deletion proposal plus one insertion
proposal. Changes to options, comments, bibliography, decision audit, roots,
structure, non-suggestion formatting, foreign suggestions, or unresolved Yjs
dependencies return FORBIDDEN before the durable state changes.
Character-formatting revisions use the authenticated edit path. Before the
candidate is committed, the Office Yrs validator requires a formatting mark
with only the stable ID, optional actor ID, author, date, kind, and bounded
before snapshot. That snapshot may contain only supported bold, italic,
underline, strike, subscript, superscript, text-style, and highlight marks with
known scalar attributes. Accept/reject removes the live mark and appends the
immutable changeKind: "formatting" record in the same update. The server then
persists that standard Yjs update before acknowledgement and room broadcast.
The browser can consequently import a DOCX w:rPrChange, collaborate on it,
and export native w:rPrChange again without the backend parsing OOXML.
Paragraph-formatting revisions use node attributes on canonical paragraph or
heading elements instead of an inline mark. The Office browser validator
requires one stable identity and a canonical bounded before snapshot covering
only supported paragraph properties. Accept/reject clears those live
attributes, optionally restores the full snapshot, and appends an immutable
changeKind: "paragraph-formatting" record atomically. Standard Yjs/Yrs
persistence keeps that state restart-safe, and Rust projection recognizes the
distinct decision kind. A DOCX w:pPrChange therefore follows the same room
path and round-trips without the service parsing OOXML.
The suggestion authorizer strips only insertion and deletion proposal effects
when comparing canonical state. Existing character- and paragraph-formatting
revisions remain in that comparison and pass the relevant bounded validators,
so a suggest connection cannot add, remove, or rewrite them. Native projection
recognizes both formatting decision kinds, while the closed
document-suggestion-create and document-suggestion-decide mutation payloads
remain insertion/deletion-only.
Attach a native agent
Issue an actorKind: "agent" ticket after authorizing the agent for the file.
The WebSocket endpoint accepts the same Office envelope from browser and native
participants. a3s-office collab session intentionally exposes a JSONL
host-channel instead of opening its own network provider: relay its outbound
records as collaboration.document events, and pass room document events back
as receive records on stdin. Start the session with --actor-name set to the
ticket's exact actorName to enable native Presence. Relay
outbound-awareness as collaboration.awareness, pass room Awareness back as
receive-awareness, and translate collaboration.peer-left into peer-left:
The native Presence controller emits browser-compatible actor, mode, activity,
and format-location state from a separate in-memory Yrs Awareness peer. The
service validates its actor ID, kind, display name, mode, artifact, namespace,
and sender client ID against the signed ticket before fan-out. Presence and
Awareness clocks never enter the agent replica or the service's durable room
store.
The process bridge must use ready.clientId in collaboration.hello and every
room envelope. Presence-enabled sessions generate this connection ID afresh;
ready.replicaClientId is only the stable Yrs author ID inside the durable
replica.
Keep each agent's actor-scoped replica separate from the service's internal
data_dir. The host owns the small JSONL/WebSocket process bridge and its
reconnect lifecycle; the example owns the complete server-side room,
authorization, persistence, fan-out, and protocol validation boundary.
Native agents use the same closed review contract as browser editors.
document-suggestion-create requires an actor-scoped suggest replica and
derives actorId from its manifest. document-suggestion-decide requires an
edit replica, exact proposal identity and text from projection v3, and may
accept or reject a replacement's insertion/deletion pair atomically. The
resulting standard Yjs update travels through the same authenticated WebSocket
room, semantic authorization, durable store, acknowledgement, and fan-out path
as a browser update. See
Suggest changes to a Document
for complete CLI and MCP payloads; agents must not construct private
ProseMirror/Yjs marks.
Reconnect and persistence semantics
Local offline changes stay in the browser Y.Doc. The adapter deliberately
does not queue every dropped WebSocket frame. After reconnect, the browser and
durable server replica each send a state vector, so both sides transfer exactly
the updates the other side is missing. Repeated delivery is safe because the
server derives a stable operation ID from room identity, sender client ID,
message kind, and update bytes.
The server persists a received sync-step-2 or update before broadcasting it
and then emits collaboration.ack. Awareness is kept only in memory. When a
socket disconnects, the server publishes collaboration.peer-left, allowing
every remaining browser to remove the stale Awareness client immediately.
Security and deployment boundary
The sample fails closed across ticket, URL path, protocol version, artifact kind, namespace, Yjs client ID, Office presence actor/name, session mode, Origin, payload size, and ticket expiration. Comment and suggestion mutations also pass semantic candidate-state authorization under the same lock used for persistence. Client-supplied origin metadata is replaced with attribution derived from the signed ticket. Durable receipts retain that trusted host authorization metadata for audit and reconnect.
The checked-in topology is complete for one service process. A multi-replica
deployment must use sticky room routing or add A3S Boot Redis/NATS fan-out and a
shared store with one writer/lock policy. Production deployments must also use
wss://, rate-limit ticket issuance and room fan-out, and keep the ticket
issuer behind the host application's authenticated backend.
Verify
Continue with Real-time collaboration for each editor's shared data model, bootstrap rules, native mutations, and framework bindings.
