WebSocket Push Extension
Capability URI: https://specs.serverlessinbox.com/websocket
Status: Draft
Background
Section titled “Background”RFC 8620 §7 defines an EventSource-based push mechanism. This extension defines an alternative using a persistent WebSocket connection, which suits environments where Server-Sent Events (SSE) are unavailable or undesirable (e.g. certain CDN/proxy configurations, mobile clients).
The protocol uses a simple JSON envelope with a oneof-style discriminator: each
message is a JSON object with exactly one of the known keys (subscribe, subscribed,
stateChange, error).
Capability object
Section titled “Capability object”A server that supports this extension includes the capability URI in its session
capabilities object. The capability value contains the WebSocket endpoint URL and
the server’s subscription limit.
{ "capabilities": { "https://specs.serverlessinbox.com/websocket": { "url": "wss://ws.serverlessinbox.com", "maxSubscriptions": 10 } }}| Property | Type | Description |
|---|---|---|
url | string | The WebSocket endpoint to connect to. |
maxSubscriptions | integer | Maximum number of JMAP data types that can be listed in a single subscribe message. |
Connection lifecycle
Section titled “Connection lifecycle”- Client opens a WebSocket connection to the
urlfrom the capability object. - Client sends one or more
subscribemessages, one per account. - Server responds with
subscribedfor each successful subscription. - Server sends
stateChangemessages whenever object state changes on a subscribed account. - Either party may close the connection at any time.
A single connection can hold subscriptions for multiple accounts.
Message types
Section titled “Message types”All messages are JSON text frames. Each frame contains exactly one top-level key that identifies the message type.
subscribe (client → server)
Section titled “subscribe (client → server)”Request a subscription to state changes for an account.
{ "subscribe": { "id": "sub-1", "accountId": "u1", "types": ["Email", "Mailbox"] }}| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Client-generated correlation id. Returned in the subscribed confirmation. |
accountId | string | yes | JMAP account id to subscribe to. |
types | string[] | no | JMAP data types to receive changes for (e.g. ["Email", "Mailbox"]). Empty array or omitted means all types. |
subscribed (server → client)
Section titled “subscribed (server → client)”Confirms that a subscribe request was accepted.
{ "subscribed": { "id": "sub-1" }}| Field | Type | Description |
|---|---|---|
id | string | The id from the corresponding subscribe message. |
stateChange (server → client)
Section titled “stateChange (server → client)”Notifies the client that one or more JMAP data types have a new state on an account.
The client should follow up with the appropriate */changes calls.
{ "stateChange": { "accountId": "u1", "changes": { "Email": "state-abc123", "Mailbox": "state-def456" } }}| Field | Type | Description |
|---|---|---|
accountId | string | The account where changes occurred. |
changes | { [dataType: string]: string } | Map of JMAP data type name to its new state string. |
The changes map contains only the types that actually changed. Clients must not
assume that a type is unchanged if it is absent from a given stateChange message.
error (server → client)
Section titled “error (server → client)”Sent when a client message cannot be processed.
{ "error": { "id": "sub-1", "code": "forbidden", "description": "Account not accessible." }}| Field | Type | Description |
|---|---|---|
id | string | Correlation id from the triggering message, if applicable. Empty string if not applicable. |
code | string | Stable error code (see Error codes below). |
description | string | Human-readable description. Not intended for programmatic use. |
Error codes
Section titled “Error codes”| Code | Description |
|---|---|
forbidden | The client does not have permission to subscribe to the requested account. |
invalidArguments | The subscribe message is missing required fields or contains invalid values. |
accountNotFound | The requested account does not exist. |
tooManySubscriptions | The types array in the subscribe message exceeds the server’s maxSubscriptions limit. |
serverFail | An unexpected server error occurred. The client may retry. |
Relationship to JMAP state
Section titled “Relationship to JMAP state”stateChange messages carry new state strings, not object diffs. Upon receiving one,
the client must call the appropriate JMAP */changes method using the new state to
fetch the actual ids that changed, then fetch the changed objects with */get.
The typical flow for an Email state change:
stateChange → Email/changes (sinceState: previousState, newState from message) → Email/get (ids: created + updated from changes response)