# 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 `/v1` and `/v2` routes. - 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 `t` for 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 `update` event with a sequence number. Presence and usage are `ephemeral` to avoid state confusion and minimize storage. - **Monotonic ordering at the user level.** `UpdatePayload.seq` is 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 primarily `POST`, with `DELETE` used 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 to `GET` + `POST` (plus occasional `DELETE`) 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 `. 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: "", clientType: "user-scoped" | "session-scoped" | "machine-scoped", sessionId?: "", machineId?: "" } ``` Rules enforced server-side: - `token` is required. - `session-scoped` requires `sessionId`. - `machine-scoped` requires `machineId`. ### 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-session` - `body`: `{ t: "new-session", id, seq, metadata, metadataVersion, agentState, agentStateVersion, dataEncryptionKey, active, activeAt, createdAt, updatedAt }` - `update-session` - `body`: `{ t: "update-session", id, metadata?, agentState? }` - `metadata`: `{ value, version }` or null - `agentState`: `{ value, version }` or null - `delete-session` - `body`: `{ t: "delete-session", sid }` - `new-message` - `body`: `{ t: "new-message", sid, message: { id, seq, content, localId, createdAt, updatedAt } }` - `update-account` - `body`: `{ t: "update-account", id, settings?, github? }` - `new-machine` - `body`: `{ t: "new-machine", machineId, seq, metadata, metadataVersion, daemonState, daemonStateVersion, dataEncryptionKey, active, activeAt, createdAt, updatedAt }` - `update-machine` - `body`: `{ t: "update-machine", machineId, metadata?, daemonState?, activeAt? }` - `new-artifact` - `body`: `{ t: "new-artifact", artifactId, seq, header, headerVersion, body, bodyVersion, dataEncryptionKey, createdAt, updatedAt }` - `update-artifact` - `body`: `{ t: "update-artifact", artifactId, header?, body? }` - `delete-artifact` - `body`: `{ t: "delete-artifact", artifactId }` - `relationship-updated` - `body`: `{ t: "relationship-updated", uid, status, timestamp }` - `new-feed-post` - `body`: `{ t: "new-feed-post", id, body, cursor, createdAt }` - `kv-batch-update` - `body`: `{ 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-message` update to other connections. - `session-alive` - `{ sid, time, thinking? }` - Emits `ephemeral` activity to user-scoped connections. - `session-end` - `{ sid, time }` - Marks session inactive and emits `ephemeral` activity. - `usage-report` - `{ key, sessionId?, tokens, cost }` - Stores usage report and optionally emits `ephemeral` usage for the session. - `machine-alive` - `{ machineId, time }` - Emits `ephemeral` machine-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? }` where `header` and `body` include `data` + `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 emits `rpc-registered` - `rpc-unregister` - `{ method }` -> server emits `rpc-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.seq` is the per-user update sequence (monotonic) used for sync ordering. - Sessions, machines, and artifacts have their own `seq` fields used by clients for ordering. - Versioned fields (metadata, agentState, daemonState, artifact header/body, access keys, KV) use optimistic concurrency with `expectedVersion` and 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`