docs/protocol.md
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.
/v1 and /v2 routes./v1/updates (transports: websocket, polling).* (server-side).The protocol is designed to stay minimal, explicit, and resilient under intermittent connectivity. A few guiding principles shape naming, payloads, and versioning:
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.update event with a sequence number. Presence and usage are ephemeral to avoid state confusion and minimize storage.UpdatePayload.seq is a single per-user counter. This makes client reconciliation simple: apply updates in order and you are consistent for that user.expectedVersion. This prevents silent overwrites and keeps conflict resolution client-driven.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?
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.
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:
token is required.session-scoped requires sessionId.machine-scoped requires machineId.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.The server emits two event types:
updatePersistent sync events. Payload shape:
{
id: string,
seq: number,
body: { t: string, ... },
createdAt: number
}
ephemeralTransient presence/usage events. Payload shape:
{
type: string,
...
}
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 nullagentState: { value, version } or nulldelete-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 }] }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 }ping -> callback {}
update-metadata
{ sid, metadata, expectedVersion }{ result: "success", version, metadata } or { result: "version-mismatch", version, metadata }update-state
{ sid, agentState, expectedVersion }{ result: "success", version, agentState } or { result: "version-mismatch", version, agentState }message
{ sid, message, localId? }new-message update to other connections.session-alive
{ sid, time, thinking? }ephemeral activity to user-scoped connections.session-end
{ sid, time }ephemeral activity.usage-report
{ key, sessionId?, tokens, cost }ephemeral usage for the session.machine-alive
{ machineId, time }ephemeral machine-activity.machine-update-metadata
{ machineId, metadata, expectedVersion }{ result: "success", version, metadata } or { result: "version-mismatch", version, metadata }machine-update-state
{ machineId, daemonState, expectedVersion }{ result: "success", version, daemonState } or { result: "version-mismatch", version, daemonState }artifact-read
{ artifactId }{ result: "success", artifact } or { result: "error", message }artifact-create
{ id, header, body, dataEncryptionKey }{ result: "success", artifact } or { result: "error", message }artifact-update
{ artifactId, header?, body? } where header and body include data + expectedVersion{ result: "success", header?, body? } or { result: "version-mismatch", header?, body? }artifact-delete
{ artifactId }{ result: "success" } or { result: "error", message }access-key-get
{ sessionId, machineId }{ ok: true, accessKey? } or { ok: false, error }rpc-register
{ method } -> server emits rpc-registeredrpc-unregister
{ method } -> server emits rpc-unregisteredrpc-call
{ method, params } -> callback { ok, result? | error? }rpc-request (ack-based).See api.md for the full HTTP endpoint catalog and auth flows.
UpdatePayload.seq is the per-user update sequence (monotonic) used for sync ordering.seq fields used by clients for ordering.expectedVersion and return a version-mismatch response containing the current version/data.packages/happy-server/sources/app/api/routespackages/happy-server/sources/app/api/socketpackages/happy-server/sources/app/events/eventRouter.ts