8.9 KiB
Protocol
This document describes the Happy wire protocol as implemented in packages/happy-server. The protocol is intentionally small: JSON over HTTP for reads/actions and Socket.IO for real-time sync. Most payloads are end-to-end encrypted client-side; see encryption.md for the encryption boundaries and encoding details. For the full HTTP surface and auth flows, see api.md.
Transport and versioning
- HTTP API: JSON requests/responses on
/v1and/v2routes. - WebSocket: Socket.IO server at path
/v1/updates(transports: websocket, polling). - CORS:
*(server-side).
Protocol design motivations
The protocol is designed to stay minimal, explicit, and resilient under intermittent connectivity. A few guiding principles shape naming, payloads, and versioning:
- Small surface area over completeness. Routes and events exist only when they provide a clear sync primitive (e.g., sessions, artifacts, KV). If a capability can be expressed as data within an existing primitive, it should be.
- Explicit event types and short keys. Update payloads use
tfor the event type and concise field names (sid,id,seq) to keep message size down without hiding meaning. These names are stable because they are used across clients. - Separation of persistent vs. ephemeral. Anything that must be recoverable after reconnect is an
updateevent with a sequence number. Presence and usage areephemeralto avoid state confusion and minimize storage. - Monotonic ordering at the user level.
UpdatePayload.seqis a single per-user counter. This makes client reconciliation simple: apply updates in order and you are consistent for that user. - Optimistic concurrency by default. Versioned fields (metadata, agent state, artifact parts, access keys, KV) require
expectedVersion. This prevents silent overwrites and keeps conflict resolution client-driven. - Client-side encryption boundaries. The server never needs to understand plaintext. The protocol therefore treats most payloads as opaque strings or base64 blobs, which keeps server logic simple and privacy guarantees strong.
- Backward compatibility over breaking changes. New routes/events are added rather than mutating existing shapes in incompatible ways. When dual behavior is needed (e.g., machines), the server emits both old and new updates.
- Avoid full REST verbs. Reads are primarily
GET, while writes/actions are primarilyPOST, withDELETEused when the intent is unambiguous. We avoid the full REST palette because many mutations are not cleanly tied to a single entity or involve more than CRUD logic. Keeping toGET+POST(plus occasionalDELETE) makes the client simpler and the protocol clearer.
If a new protocol field or event is proposed, it should answer: does this create a durable sync primitive, or can it be encoded inside existing encrypted payloads without expanding the API surface?
Authentication
Most endpoints require Authorization: Bearer <token>. The same token is also used in the Socket.IO handshake. Full auth flows and endpoints are documented in api.md.
WebSocket connection
Handshake
Connect with Socket.IO using:
path: "/v1/updates"
auth: {
token: "<bearer token>",
clientType: "user-scoped" | "session-scoped" | "machine-scoped",
sessionId?: "<session id>",
machineId?: "<machine id>"
}
Rules enforced server-side:
tokenis required.session-scopedrequiressessionId.machine-scopedrequiresmachineId.
Connection types
user-scoped: receives account-wide updates.session-scoped: receives updates for a specific session only.machine-scoped: used by daemons; receives machine updates and emits machine state.
Server -> client events
The server emits two event types:
update
Persistent sync events. Payload shape:
{
id: string,
seq: number,
body: { t: string, ... },
createdAt: number
}
ephemeral
Transient presence/usage events. Payload shape:
{
type: string,
...
}
Update event types
Field names below match on-wire payloads.
-
new-sessionbody:{ t: "new-session", id, seq, metadata, metadataVersion, agentState, agentStateVersion, dataEncryptionKey, active, activeAt, createdAt, updatedAt }
-
update-sessionbody:{ t: "update-session", id, metadata?, agentState? }metadata:{ value, version }or nullagentState:{ value, version }or null
-
delete-sessionbody:{ t: "delete-session", sid }
-
new-messagebody:{ t: "new-message", sid, message: { id, seq, content, localId, createdAt, updatedAt } }
-
update-accountbody:{ t: "update-account", id, settings?, github? }
-
new-machinebody:{ t: "new-machine", machineId, seq, metadata, metadataVersion, daemonState, daemonStateVersion, dataEncryptionKey, active, activeAt, createdAt, updatedAt }
-
update-machinebody:{ t: "update-machine", machineId, metadata?, daemonState?, activeAt? }
-
new-artifactbody:{ t: "new-artifact", artifactId, seq, header, headerVersion, body, bodyVersion, dataEncryptionKey, createdAt, updatedAt }
-
update-artifactbody:{ t: "update-artifact", artifactId, header?, body? }
-
delete-artifactbody:{ t: "delete-artifact", artifactId }
-
relationship-updatedbody:{ t: "relationship-updated", uid, status, timestamp }
-
new-feed-postbody:{ t: "new-feed-post", id, body, cursor, createdAt }
-
kv-batch-updatebody:{ t: "kv-batch-update", changes: [{ key, value, version }] }
Ephemeral event types
activity:{ type: "activity", id: sessionId, active, activeAt, thinking? }machine-activity:{ type: "machine-activity", id: machineId, active, activeAt }usage:{ type: "usage", id: sessionId, key, tokens, cost, timestamp }machine-status:{ type: "machine-status", machineId, online, timestamp }
Client -> server WebSocket events
-
ping-> callback{} -
update-metadata{ sid, metadata, expectedVersion }- Response:
{ result: "success", version, metadata }or{ result: "version-mismatch", version, metadata }
-
update-state{ sid, agentState, expectedVersion }- Response:
{ result: "success", version, agentState }or{ result: "version-mismatch", version, agentState }
-
message{ sid, message, localId? }- Creates a new session message (encrypted payload) and emits
new-messageupdate to other connections.
-
session-alive{ sid, time, thinking? }- Emits
ephemeralactivity to user-scoped connections.
-
session-end{ sid, time }- Marks session inactive and emits
ephemeralactivity.
-
usage-report{ key, sessionId?, tokens, cost }- Stores usage report and optionally emits
ephemeralusage for the session.
-
machine-alive{ machineId, time }- Emits
ephemeralmachine-activity.
-
machine-update-metadata{ machineId, metadata, expectedVersion }- Response:
{ result: "success", version, metadata }or{ result: "version-mismatch", version, metadata }
-
machine-update-state{ machineId, daemonState, expectedVersion }- Response:
{ result: "success", version, daemonState }or{ result: "version-mismatch", version, daemonState }
-
artifact-read{ artifactId }- Response:
{ result: "success", artifact }or{ result: "error", message }
-
artifact-create{ id, header, body, dataEncryptionKey }- Response:
{ result: "success", artifact }or{ result: "error", message }
-
artifact-update{ artifactId, header?, body? }whereheaderandbodyincludedata+expectedVersion- Response:
{ result: "success", header?, body? }or{ result: "version-mismatch", header?, body? }
-
artifact-delete{ artifactId }- Response:
{ result: "success" }or{ result: "error", message }
-
access-key-get{ sessionId, machineId }- Response:
{ ok: true, accessKey? }or{ ok: false, error }
-
rpc-register{ method }-> server emitsrpc-registered
-
rpc-unregister{ method }-> server emitsrpc-unregistered
-
rpc-call{ method, params }-> callback{ ok, result? | error? }- Server forwards to the registered socket via
rpc-request(ack-based).
HTTP endpoints by area
See api.md for the full HTTP endpoint catalog and auth flows.
Sequencing and concurrency
UpdatePayload.seqis the per-user update sequence (monotonic) used for sync ordering.- Sessions, machines, and artifacts have their own
seqfields used by clients for ordering. - Versioned fields (metadata, agentState, daemonState, artifact header/body, access keys, KV) use optimistic concurrency with
expectedVersionand return a version-mismatch response containing the current version/data.
Implementation references
- API routes:
packages/happy-server/sources/app/api/routes - Socket handlers:
packages/happy-server/sources/app/api/socket - Event routing:
packages/happy-server/sources/app/events/eventRouter.ts