Part of Stem. This page specifies the remote procedure calls of the sync protocol: the peer-to-peer methods two daemons use with each other, and the local methods a client uses with its own daemon. Every method and every message type is a Hypermedia Schema published as a page of this site, and the union of all methods is rpc/method.
Transport
Peer methods run as gRPC over a libp2p stream with the protocol id /hypermedia/1.0. Two peers speak only when they share the exact id; the id changes whenever a message layout or the scope-set rule changes. The stream is secured by libp2p (the device keys), so the gRPC layer carries no transport credentials, and the server reads the caller's peer id from the connection. Messages are at most 8 MiB in either direction.
Local methods run over the daemon's own gRPC and HTTP endpoints, authenticated by the daemon's local token or a bearer token as described in Daemon. Over HTTP a read is GET /api/<Key> with the input in the query string and a write is POST /api/<Key> with a DAG-CBOR body, as the Seed API already does.
Every method is one call: request(key, input) → output. The schema of a method is a closed struct with exactly three required properties, key (a literal naming the method), input and output. A streaming method, of which Watch is the only one, returns one output value per message for the life of the stream.
Errors are a small, closed set. A method either returns its output or fails with one of:
error | meaning |
|---|---|
| the method needs an account binding on the connection and there is none |
| the input does not match its schema or violates a stated rule |
| a limit was hit; retry later |
| the server cannot answer now, for example during a reindex |
There is deliberately no not-found and no permission-denied: a blob or resource the caller may not read is reported in the output exactly like one the server does not have (uniform denial).
Authentication model
A connection starts with no identity beyond its peer id. Authenticate binds it to an account by a signature over the account, the two peer ids and a timestamp; a connection may be bound to several accounts; bindings end with the connection or with Goodbye. Every peer method is then evaluated against the readers sets that include any bound account, plus everything readable by everyone. The same evaluator answers the local Access method, so a client can see in advance what a connection will be served.
Methods
key | side | input | output | does |
|---|---|---|---|---|
peer | account, ts, sig | expires | binds the connection to an account | |
peer | spaces, nextPageToken | spaces the caller may read | ||
peer | page, listHash | peers, nextPageToken | peer-table exchange | |
peer | ranges | one round of set reconciliation | ||
peer | scope, cids | blobs, missing | serves readable blobs authority-first | |
peer | scope, cids, proof | id, wanted, rejected | offers blobs for a scope | |
peer, stream | scope, since | notification per message | live updates on a scope | |
peer | none | none | ends bindings and watches | |
local | scope, version, wait | bring a scope up to date now | ||
local | scope | sync-status | report without starting a run | |
local | account | the policy in force | ||
local | account, rules | policy node-ref | publish a new policy | |
local | scope, principal | explain access | ||
local | blobs, scope | cids, stashed, rejected | ingest signed blobs | |
local | filters, page | disclosures, nextPageToken | read the disclosure ledger | |
local | filters, page | transfers, nextPageToken | read the transfer log | |
local | addrs | peer | dial a peer explicitly |
The peer methods are the whole wire surface. There is no listing of all blobs, no push of arbitrary blobs, no per-space mirror mode and no subscription message; a subscribed peer is simply one that reconciles and watches.
Conventions
A method schema is a closed struct {key, input, output}, all three required, the convention introduced for the Seed API in the rpc/* library work. key is a literal so the method union is discriminated on it.
input and output are either inline structs or includes of a shared type under rpc/type/* or data/*.
A field that is always present but may be empty is spelled anyOf [T, null] and marked required, so the receiver can rely on the key.
Lists are bounded where the protocol needs a bound; the bound is in the schema (maxItems).
Pagination uses page in and nextPageToken out; a null token means the last page.
Timestamps are Unix milliseconds (timestamp). Clocks are advisory everywhere except Authenticate's one-minute window and a grant's expires.
Messages are DAG-CBOR on the wire and dag-json in examples.
A worked exchange
Peer A follows the space z6MkA… and reconnects to its site, peer B.
{"key": "Authenticate", "input": {"account": {"/": {"bytes": "7QEA…"}}, "ts": 1759910400000, "sig": {"/": {"bytes": "…"}}}}
{"expires": 1759914000000}A opens the first reconciliation round with its whole set as sixteen fingerprints.
{"key": "Reconcile", "input": {
"scope": {"space": {"/": {"bytes": "7QEA…"}}, "depth": "subtree"},
"ranges": [
{"mode": "fingerprint", "boundTs": 1759000000000, "boundCid": {"/": "bafy…01"}, "fingerprint": {"/": {"bytes": "…"}}},
{"mode": "fingerprint", "boundTs": 1759500000000, "boundCid": {"/": "bafy…02"}, "fingerprint": {"/": {"bytes": "…"}}},
{"mode": "fingerprint", "boundTs": 1759910400000, "fingerprint": {"/": {"bytes": "…"}}}
]}}B agrees on the first two ranges and splits the third, which does not match.
{"ranges": [
{"mode": "skip", "boundTs": 1759500000000, "boundCid": {"/": "bafy…02"}},
{"mode": "fingerprint", "boundTs": 1759800000000, "boundCid": {"/": "bafy…07"}, "fingerprint": {"/": {"bytes": "…"}}},
{"mode": "list", "boundTs": 1759910400000, "cids": [{"/": "bafy…11"}, {"/": "bafy…12"}, {"/": "bafy…13"}]}
]}A answers the list with its own list for that range; after the third round the ranges all agree and A knows it lacks bafy…12 (a Grant) and bafy…13 (a Node). It fetches them.
{"key": "Fetch", "input": {"scope": {"space": {"/": {"bytes": "7QEA…"}}, "depth": "subtree"}, "cids": [{"/": "bafy…13"}, {"/": "bafy…12"}]}}
{"blobs": [
{"cid": {"/": "bafy…12"}, "data": {"/": {"bytes": "…Grant…"}}},
{"cid": {"/": "bafy…13"}, "data": {"/": {"bytes": "…Node…"}}}
], "missing": []}The Grant came first although A asked for the Node first. B wrote two disclosures, both with basis grant naming A's account and the grant that makes it a reader. A then watches.
{"key": "Watch", "input": {"scope": {"space": {"/": {"bytes": "7QEA…"}}, "depth": "subtree", "facets": ["comments"]}}}
{"scope": {"space": {"/": {"bytes": "7QEA…"}}, "depth": "subtree", "facets": ["comments"]}, "cids": [{"/": "bafy…21"}, {"/": "bafy…22"}], "ts": 1759910460000}A new comment's Node and Snapshot arrived at B one minute later; A fetches them and shows the comment.
Versioning
Adding a method is adding a schema page and an arm to rpc/method; a peer that does not know a key fails the call with invalid-argument and nothing else changes. Adding an optional field to an input or output is compatible. Changing or removing a field, changing the scope-set rule, the item order or the fingerprint of Reconcile, or the authority-first order of Fetch, is a protocol change: it takes a new protocol id, and peers on different ids do not sync.
See also
The sync protocol, the behaviour these methods implement.
Privacy, for what the ledgers record.
Network, the shipping protocol.
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime