This document defines the realtime protocol currently implemented by:
/bustransport inapps/web/src/routes/bus.ts/yjs/$co-bus transport inapps/web/src/routes/yjs.$.ts
Primary schema source-of-truth is apps/web/src/lib/types.ts (HelloSchema, GSMessageSchema).
/bus(JSON + binary)/yjs/<docName>(Yjs binary sync/awareness)
editorwallcontrollergallery
All sockets receive server_hello first:
server_hello(S->C):{ type:'server_hello', commit, builtAt }
All authenticated clients then use the hello flow:
- client sends
hello - server sends
hello_challenge(non-editor roles) - client sends
hello_authproof - server sends
hello_authenticatedor closes /auth_denied
- editor:
{ type:'hello', specimen:'editor' } - wall:
{ type:'hello', specimen:'wall', wallId, col, row, devicePublicKey? } - controller:
{ type:'hello', specimen:'controller', wallId, devicePublicKey? } - gallery:
{ type:'hello', specimen:'gallery', wallId?, devicePublicKey? }
hello_challenge(S->C):{ type:'hello_challenge', nonce }hello_auth(C->S):proof.signature?(device challenge signature)proof.portalToken?(controller-only fallback)- at least one proof field is required
hello_authenticated(S->C)auth_denied(S->C):{ type:'auth_denied', reason?: 'missing_session' }device_enrollment(S->C):{ type:'device_enrollment', id }(for pending devices)
Editors join content scope after auth using:
switch_scope(C->S):{ projectId, commitId, slideId }
Editors may also leave scope explicitly:
leave_scope(C->S)
rehydrate_please(C->S)hydrate(S->C)upsert_layer(bi-directional via server routing)delete_layer(bi-directional via server routing)seed_scope(editor -> server)clear_stage(editor -> server)
bind_wall(controller/gallery/admin paths)request_bind_wall(editor takeover-aware flow)unbind_wallwall_binding_status(editor/controller-facing)wall_node_count(editor-facing)wall_binding_changed(gallery-facing)wall_unbound(gallery-facing)
Gallery clients receive and act on:
gallery_state(snapshot of walls + published projects)wall_binding_changed(incremental binding update)wall_unbound(explicit unbind notification)projects_changed(published project list changed)bind_override_requested(editor takeover request)bind_override_result(final override decision/result)
Gallery clients can send:
bind_override_decision(allow: true|false)unbind_wallbind_wall(gallery-sourced binding)
video_playvideo_pausevideo_seekvideo_sync
stage_dirtystage_savestage_save_response
update_slidesslides_updatedasset_addedprocessing_progress
reboot- legacy JSON
ping/pongschema entries remain inGSMessageSchema(clock sync is binary in runtime)
- editor sends
request_bind_wall - if no conflict, server binds immediately and returns
bind_override_result(reason:'not_required') - if conflict with active gallery approver, server sends
bind_override_requestedto gallery peers for that wall - gallery sends
bind_override_decision - server responds with
bind_override_result(approved|denied|timeout|invalid|unknown_wall)
editor:*-> persistent scope update + scope fanoutcontroller:add_line_layer-> transient wall-local overlay path (no DB persistence)yjs:sync-> Yjs bridge text-layer update
All numeric fields are little-endian.
Opcodes:
0x05SPATIAL_MOVE0x08CLOCK_PING0x09CLOCK_PONG0x15VIDEO_SYNC
Reserved for future binary migrations:
0x10UPSERT_LAYER0x11DELETE_LAYER0x12VIDEO_PLAY0x13VIDEO_PAUSE0x14VIDEO_SEEK
CLOCK_PINGframe:opcode(u8) + t0(f64)CLOCK_PONGframe:opcode(u8) + t0(f64) + t1(f64) + t2(f64)- clients estimate offset/RTT from returned times
- header:
opcode(u8) + count(u16) - repeated entry (
30bytes each):numericId(u16)cx(f32)cy(f32)width(f32)height(f32)scaleX(f32)scaleY(f32)rotation(f32)
- header:
opcode(u8) + count(u16) - repeated entry (
19bytes each):numericId(u16)status(u8)(1=playing,0=paused)anchorMediaTime(f64)anchorServerTime(f64)
Yjs route is apps/web/src/routes/yjs.$.ts and is handled by YCrossws.
Underlying protocol message types:
- sync (
messageSync = 0) - awareness (
messageAwareness = 1)
Document identity format:
${projectId}_${commitId}_${slideId}_${layerId}
Lifecycle:
- peer opens
/yjs/<docName> - sync step1 + awareness fanout
- dirty docs flush every
SYNC_INTERVAL_MS(1000ms) - persisted to
ydocs - converted to HTML and bridged to
/busthroughprocess.__YJS_UPSERT_LAYER__
- message auth is enforced in
isWsMessageAuthorized(...) - handshake rate-limited by IP (
hello,hello_auth) - mutation messages use per-peer rate limiting and strike tracking
- binary
SPATIAL_MOVEchecks sender role/permissions before relay
- Prefer additive schema changes (new optional fields/messages).
- Keep binary opcode semantics stable once deployed.
- If behavior changes, update this document and
docs/BUS_PIPING.mdtogether. - Validate protocol changes against
GSMessageSchemaandHelloSchemain the same PR.