HighX
Reference

Protocol

Version 1 wire frames.

WebSocket is the only required carrier in v1. SSE and long-polling are later adapters.

client -> hello | auth | sub | unsub | mutate | action | resume | ping
       -> doc.open | doc.submit | doc.presence | doc.close
       -> run.watch | run.unwatch
server -> welcome | snapshot | patch | stream | error | bye | pong
       -> doc.reset | doc.commit | doc.reject | doc.awareness
       -> run.delta

decodeFrame() validates untrusted input and rejects unknown versions, discriminants, fields with invalid types, and unsafe sequence values. A frame the client cannot parse is dropped rather than applied.

Completions

A mutation is answered with a stream frame carrying done: true. Its delta holds the handler's return value as JSON, or is empty when the handler returned nothing — which is not the same as a value being dropped in transit.

A client that ignores delta makes every mutation resolve with undefined, so an id from an insert is reachable over HTTP and not over the connection the application actually uses.

Error frames

An error frame may carry a requestId, a subscriptionId, or neither:

FieldMeaning
requestIdThe mutation or action that failed
codeWhat kind of failure, from HighXErrorCode — the part a client branches on
subscriptionIdThe subscription that failed, typically an unresumable cursor
neitherA connection-level failure

The subscription form exists so a client can recover the exact subscription whose resume failed, rather than inferring it from a timeout.

Document frames

Collaborative fields do not travel the subscription path. A subscription is a query whose answer can always be recomputed; a document is an ordered stream where a client may hold the only copy of part of it.

FrameDirectionCarries
doc.openclientdocumentType, documentId, the revision it already holds
doc.submitclientone op and the base revision it was written against
doc.presenceclientopaque awareness — a caret, an entity, a viewport
doc.closeclientleaving, so carets stop being drawn
doc.resetserverthe whole document, for a client with nothing usable to catch up from
doc.commitservera sequenced op, its revision and actor, and the document's hash
doc.rejectserveran op refused as malformed, unauthorised, or against the rules
doc.awarenessserversomeone else's awareness; null data means they left

Only doc.open names a type. Everything after it is looked up from the open the server recorded, so a client cannot name one document's type while writing to another, and writing to something it never opened is refused.

A commit goes to everyone including its sender — that echo is how the sender learns where its op landed after rebasing. Awareness goes to everyone except its sender.

Ops replayed as catch-up carry an empty hash: the log records ops, not what the document was at each revision. Do not compare an empty hash, or every reconnection reports divergence and the one alarm that catches real corruption becomes noise. The next live commit carries a real one.

Sequences

Each subscription has its own monotonic sequence within its owning Durable Object. There is no global sequence across objects, so a client maintains one cursor per subscription rather than a single global one.

Versioning

Frames are versioned in @highx/protocol, separately from the packages. A deployed server and a browser that loaded an hour ago will not always agree on which release they are running, so additive changes need a default and removals need a version bump. There is no "everyone will refresh" assumption available.

Run output

A run's state travels as ordinary subscription frames — workflowRuns is a tenant table like any other. These carry the part that is not durable.

FrameDirectionCarries
run.watchclientthe run whose output it wants, gated on the permission to start runs
run.unwatchclientgiving up interest; always accepted, since refusing only keeps a dead watcher
run.deltaserverone fragment of a run's model output, with its turn and optional stable deltaId

Ephemeral by construction, like doc.presence: never stored, never replayed, and dropped when nobody is watching. A replayed turn produces none at all, because no model call is made. Clients treat missing deltas as normal rather than as a stalled run, and use deltaId to ignore duplicate delivery without dropping legitimate repeated text fragments.

On this page