Gateway

The Gateway is LocalUniverse's WebSocket protocol for real-time events: messages arriving, presence changing, equipment updating, channels mutating. One socket per client receives everything the user is subscribed to.

It's a separate Cloudflare Workers deployment from the REST API, backed by Durable Objects. Auth happens at the upgrade — there is no IDENTIFY round-trip on this protocol. The server validates the token via a service binding to the API, then HELLO + READY fire immediately.


Connect

wss://gateway.localuniverse.io/v1?v=1&token=<token>
  • Name
    v
    Type
    integer
    Description

    Protocol version. Must equal GATEWAY_VERSION (currently 1) — anything else is rejected at the upgrade with 400.

  • Name
    token
    Type
    string
    Description

    Same bare user-session token used on REST. Lives in the query string because browsers can't set headers on a WebSocket upgrade. Invalid / revoked tokens get 401 at the upgrade.

  • Name
    resume
    Type
    string
    Description

    Optional. Existing session_id to reattach to.

  • Name
    last_seq
    Type
    integer
    Description

    Required when resume is set. Last sequence number the client successfully processed.

Browser

const ws = new WebSocket(
  `wss://gateway.localuniverse.io/v1?v=1&token=${token}`
);

Payload shape

Every frame in either direction shares one envelope:

{
  "op": 0,
  "d": {},
  "s": 1,
  "t": "MESSAGE_CREATE"
}
  • Name
    op
    Type
    integer
    Description

    Opcode. See the opcode table.

  • Name
    d
    Type
    any
    Description

    Event-specific data.

  • Name
    s
    Type
    integer
    Description

    Sequence number. Set on dispatch frames (op: 0); used by RESUME to replay. Omitted on non-dispatch frames.

  • Name
    t
    Type
    string
    Description

    Event name. Set on dispatch frames (op: 0); omitted otherwise. See Gateway events for the full list.


Opcodes

OpNameDirectionNotes
0DISPATCHserver → clientEvent push. t carries the event name.
1HEARTBEATbidirectionalClient sends every heartbeat_interval ms.
2IDENTIFYclient → serverReserved; the current protocol authenticates at the upgrade and skips this.
3PRESENCE_UPDATEclient → serverUpdate the caller's presence status.
4SUBSCRIBE_CHANNELclient → serverSubscribe to a channel topic — messages, typing, channel updates.
5UNSUBSCRIBE_CHANNELclient → serverDrop a channel subscription.
6TYPINGclient → serverStart a typing indicator in a channel.
7RESUMEclient → serverReattach using a prior session_id + last_seq.
8WARP_UPDATEclient → serverPosition / pose update for the in-world avatar.
10HELLOserver → clientFirst frame the server sends. Carries heartbeat_interval.
11HEARTBEAT_ACKserver → clientAcknowledges a client heartbeat.
12INVALID_SESSIONserver → clientSent during a failed RESUME to tell the client whether it can retry.

Connection lifecycle

1. Upgrade

Client opens the WebSocket with ?v=1&token=.... The Gateway validates the token by calling /v1/auth/verify on the API via service binding. On failure the upgrade returns 401.

2. HELLO

The server immediately dispatches:

{ "op": 10, "d": { "heartbeat_interval": 30000 } }

heartbeat_interval is the cadence the client must keep, in milliseconds. The server times out a socket whose last activity is older than 90_000 ms (three missed heartbeats).

3. READY

For a fresh connection (no ?resume), the server dispatches READY next:

{
  "op": 0,
  "s": 1,
  "t": "READY",
  "d": {
    "user": { "id": "...", "email": "...", "display_name": "...", ... },
    "session_id": "31293...",
    "heartbeat_interval": 30000
  }
}

The user field is the same shape REST returns from /v1/users/@me — clients can hydrate the entire app state from this one frame.

Persist session_id immediately. If the connection drops, RESUME uses it to replay missed events.

4. Heartbeat loop

{ "op": 1 }

Client sends every heartbeat_interval ms. Server replies { "op": 11 }. Both sides treat lack of activity for 3 × interval as a dead connection.


Resume

If the socket drops and the client still has session_id + the last s it processed, reconnect with ?resume:

wss://gateway.localuniverse.io/v1?v=1&token=...&resume=31293...&last_seq=4287

The Gateway routes to the same Durable Object instance that owned the original socket. If the DO still has the in-memory buffer and your last_seq falls inside it, you get RESUMED followed by a replay of every event with s > last_seq. Once the replay drains, normal dispatch resumes.

If the DO has been evicted, or the buffer doesn't reach back to last_seq, the server sends:

{ "op": 12, "d": { "resumable": false } }

The client should drop the cached session_id, reconnect without ?resume, and re-hydrate from READY.


Subscriptions

A client only receives events for resources it has subscribed to. The Gateway maintains per-channel subscriber sets in a ChannelTopic Durable Object.

Subscribe to a channel. d is the channel id as a snowflake string. Once subscribed you receive MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, TYPING_START, and CHANNEL_UPDATE for that channel.

SUBSCRIBE_CHANNEL

{ "op": 4, "d": "3123..." }

Drop the subscription. The server forgets about you for that channel; nothing else is required.

UNSUBSCRIBE_CHANNEL

{ "op": 5, "d": "3123..." }

User-scoped events (USER_UPDATE, EQUIPMENT_UPDATE, INVENTORY_ADD, INVENTORY_REMOVE, RELATIONSHIP_UPDATE) are auto-delivered without an explicit subscribe — they fan out to every live session for the affected user via the UserPresence DO.


Close codes

CodeMeaningReconnect behavior
4001Expected IDENTIFY got something elseLibrary bug — investigate before retry
4004Authentication failedRefresh the token and reconnect
4008IDENTIFY / first message timeoutTransient — retry with backoff
4009Session expiredReconnect (token may still be valid)

1xxx codes follow the WebSocket spec (1000 normal close, 1011 server error). Well-behaved clients should distinguish "permanent close, log the user out" from "transient close, retry with backoff" — only 4004 and a deliberate user-initiated 1000 should drop the user back to login.

Was this page helpful?