Skip to content

Wire protocol

Silt runs two lanes over one WebTransport (QUIC) connection. The protocol is self-describing, minimal, and carries JSON payloads. You don’t need this page to use @silt/client — it’s here if you’re implementing a client or server, or just want to know exactly what’s on the wire.

Unreliable, latest-wins, droppable. Carries the ~20Hz position firehose.

Framing: [1 byte type][JSON payload]

type direction payload meaning
0x01 PRESENCE client → server {state} this peer’s latest presence. The server stamps the peer’s id (known from the QUIC session).
0x01 PRESENCE server → client {id, state} a peer’s latest presence, relayed to all others.
  • The 1-byte tag is kept even though presence is the only datagram type today, so a second datagram type can be added without a wire break.
  • The server drops presence datagrams from a session that hasn’t sent hello yet (the presence-before-hello race). The lane is droppable, so this is harmless — latest-wins recovers on the next datagram.
  • No buffering, and no id needed client→server: the session identifies the peer, and the server stamps id on relay.

Lane 2 — Reliable (one bidi stream per peer)

Section titled “Lane 2 — Reliable (one bidi stream per peer)”

Ordered, no drop or reorder. Carries control frames and discrete events. The client opens one bidirectional stream right after connect; the server accepts it. All reliable traffic for that peer flows both ways on this single stream.

Framing: [4-byte big-endian uint32 length][JSON frame]. Each frame is a JSON object with a type tag: {"t": "<type>", ...}.

t fields meaning
hello {id} First frame. Stable, client-supplied identity. Binds session → id.
event {event} a discrete reliable event to broadcast to others.
bye clean voluntary leave (sent by room.close()).
t fields meaning
snapshot {self, peers: [{id, state}]} sent immediately after hello: who’s here plus each peer’s last presence (state may be null if a peer hasn’t sent presence yet).
join {id} a new peer joined.
leave {id, reason} a peer left. reason: "left" (clean bye) or "timeout" (session died without bye).
event {from, event} a peer’s reliable event, relayed.

hello.id is stable and client-supplied. Reconnecting is just calling joinRoom again with the same id.

Supersede suppression. When a hello arrives for an id that already has a live session in the room, the server replaces it in place: it swaps the session pointer for that id and sends the newcomer its snapshot, but does not broadcast a leave then join to others — no id flap. Presence and events simply resume on the new session. The old session’s teardown is marked superseded so its error-driven leave is suppressed.

If the old session is detected dead (timeout) before the rejoin arrives — a real network gap — peers honestly see leave: "timeout" and later join. That’s correct, not a flap.

Registering a new peer, building its snapshot, and broadcasting its join all happen under one room mutex. A peer joining concurrently can’t be lost from a snapshot or miss a join — there’s no lost-update window.

  • A bye frame received (or stream FIN after a prior bye) → reason: "left".
  • A stream/session error with no byereason: "timeout".