Protocol overview
rifleDB speaks two protocols on the same paths: a Socket.IO connection for realtime use (what the SDK uses) and a REST
API for everything else (curl, server code, other languages). This page is a map; the binding definitions are in
architecture.md §4, §7 and §8, and the shared types, signing-string builders and pure ops live in
@rifledb/ (packages/).
Paths and values
- Paths are slash-separated segments of
[A-Za-z0-9_(1–128 characters, not~. -] ./.), at most 16 deep and 512 characters. Top-level. _(system endpoints),systemandusersare reserved; segments starting with@name actions. - Values are JSON: no non-finite numbers, no
_/_ proto_ _ constructor/prototypekeys, depth ≤ 8, ≤ 64 KB serialized (folders may set a lowermaxValueBytes). Fast folders take only primitives and arrays of ≤ 256 primitives, ≤ 2 KB. - Every stored value has a
rev: one global, increasing sequence. Together with the server'sepochit identifies a version: ETags are"<epoch>:<rev>". The epoch changes on every server boot (INCRin Redis; a server with its own non-persistent in-memory store seeds it from the clock so it never repeats; architecture.md §3), so an ETag from before a restart no longer matches.
Realtime: Socket.IO at / v1/ _ ws
WebSocket transport only (long-polling is opt-in). Browsers must connect from an allowed origin
(RIFLE_); clients that send no Origin (Node) are admitted. Payloads are capped at 64 KB.
- In the handshake the client may send
auth: {slot: <32 random bytes,(Socket.IO'sbase64url>} authoption): a secret it keeps for its lifetime. One address may hold 64 connections at once; further handshakes are refused. - The server greets with
hello {v: 1,.epoch, nonce, serverTime, origin, limits, slot} slotis'p_(random without a secret): the key segment this connection may write in a' + b64url(sha256('rifle/ slot/ v1\n' + secret)). slice(0, 22) publicWrite: 'own'folder. The same secret gives the same slot on every connection, and nobody who only sees the slot can claim it. - The client signs
rifle/with each device or folder key and sendsws-auth/ v1\n<origin>\n<nonce>\n<serverTime>\n<kid> auth {proofs: [{kid,. Anonymous sessions skip this and can read public data and call public actions. The SDK re-authenticates after every reconnect.pub, sig}], register?} - Every client message is acknowledged:
{ok: true,or…} {ok: false,.code, msg, retryAfter?}
| Client → server | What it does |
|---|---|
sub / unsub {path, | Subscribe to a key or a prefix (never across into a nested folder); the ack has the snapshot, or with since: {epoch, only the changes after it |
get {path, | One-off read |
op {id, | One op (set, del, inc, toggle, push, unshift, removeAt, removeWhere, merge, strAppend, strPrepend); ifRev makes it conditional (E_). Ack {rev,; on fast folders also f (the folder) |
batch {id, | Several ops in one folder, all or nothing |
act {id, | Call a server action |
pub {path, | An ephemeral event (needs write access) |
ondc {path, | An op the server runs when this socket disconnects (presence cleanup), after everything the socket sent before has finished, in registration order within a folder. A set/del only replaces what this socket wrote last, with an op, a batch or an action it ran: a key written after that (e.g. by the same client over a newer connection or over REST, or by server code) is left alone |
share. / share. / share. {folder, | List, set or remove a folder's grants (callers who manage the folder) → {grants} |
share. {folder, / share. {tokenHash} | Claim invites → {link, / {revoked} |
share. {folder, / share. {kid} | Folder keys (the private half stays with the client) → {kid, / {kid, |
stepup., passkey., passkey. / passkey., recovery., admin., device. | Passkeys, step-up, account recovery, admin bootstrap, device linking (Access control) |
| Server → client | |
|---|---|
chg {f, | Changes of one folder f (flags: 1 = deleted, 2 = fast folder) |
evt {folder, | An event emitted by an action or pub |
access {path, | A subscription ended (revoked key, expired elevation, …) |
elev {until} | Elevation started/extended (until in ms) or ended (0) |
Op ids make retries safe: a replayed id returns the original result (kept 5 minutes). Fast folders coalesce updates
per path at hz per second (default 30) and send them as volatile messages, but deletes and each key's settled final
value are always delivered.
sub acks carry seq: the server's sequence (highest rev assigned) when the snapshot was read. Every change with a rev
at or below it is already in entries/dels, so a chg entry with rev <= seq arriving later is dropped — also for
keys absent from the snapshot.
E_ acks carry retryAfter (ms) and, when the server knows it, scope: which limit refused.
scope | Limit | What the SDK does |
|---|---|---|
conn | the connection's message bucket (200/s); refusal is sticky (≥ 250 ms) | keeps the request in place, pauses every request, halves window |
ip | the address's message bucket (2000/s), sticky like conn | same |
public | anonymous publicWrite buckets: 30/s per session, 200/s per address (fast folders: 1000/s per address) | drops a fast-folder write a newer one of the same key supersedes; otherwise pauses only that folder's requests, backing off |
action | the action's own rate | rejects to the caller |
folder | the folder's limits. (all writers, every transport, actions) | rejects to the caller |
REST: / v1/ …
| Request | Effect |
|---|---|
GET / | {v, with ETag; If-None-Match → 304 |
GET / | {folder, |
GET / | Server-sent events: snapshot, then chg (same tuples as the socket), : ping every 15 s, access when access ends |
PUT / | set with the raw JSON body (Content-Type: application/) |
PATCH / | application/: {op, or an array (one atomic batch); or application/ (RFC 7396) |
POST / | push with {item, |
POST / | Call an action; body = its arguments (empty = {}) → {result} |
DELETE / | del |
GET /, GET / | {ok,, {serverTime} |
POST /, / | Passkeys over REST; a step-up returns an elevationToken for the Rifle-Elevation header |
POST /, / | Invites and device linking |
GET /, POST / | Sharing, as the socket's share. messages (signed, by someone who manages the folder) |
Preconditions: If-Match: "<epoch>:<rev>" writes only if the key is still at that version (412 otherwise);
If-None-Match: * only if it does not exist. Validation and access checks run before anything is written: a refused
request leaves no trace.
Signed requests
Requests that need an identity carry:
Rifle-Timestamp: <ms since epoch>
Rifle-Nonce: <≥16 random bytes, base64url>
Content-Digest: sha-256=:<base64 of sha256(body)>: (with a body; RFC 9530)
Rifle-Signature: <kid>=<base64url Ed25519 signature>[, …] (up to 4 keys)
Rifle-Elevation: <token> (optional, after a REST step-up)over the string
rifle/http/v1\n<METHOD>\n<host>\n<path and query exactly as sent>\n<timestamp>\n<nonce>\n<Content-Digest or ->\n<sha256(elevation token) base64url or ->\n<kid>The server checks, in this order: timestamp within ±60 s (401), nonce unused (409 E_; nonces are kept 3
minutes), digest matches the raw body (422), Host is one of RIFLE_ (403), signatures (401). The
SDK's HTTP client and createRifleServer do all of this; e2e/tests/support.ts and
server/smoke.ts show it in about 30 lines with WebCrypto.
Errors
RFC 9457 application/: {type, (scope on E_
as on the socket, when known), with Retry-After on 429 and WWW-Authenticate: Rifle-Signature on 401.
| Code | HTTP | Meaning |
|---|---|---|
E_ | 422 | Bad path, value, op or arguments |
E_ | 401 | Needs a (valid) signature |
E_ | 401 | Needs a passkey step-up |
E_ | 403 | Signed in, but not allowed |
E_ | 403 | A server-only path |
E_ | 404 | No such key, folder or action |
E_ | 412 | If-Match / ifRev did not match |
E_ | 409 | Nonce used before |
E_ | 409 | Action-specific (the demo's full island) |
E_ | 413 | Too large, too many keys, over a user's storage budget |
E_ | 429 | Rate limit; retryAfter says when to try again |
E_ | 503 | Storage unreachable (writes to normal folders are refused) |
Other codes an action raises with abort answer 422; unexpected server errors 500 E_ without details.
CORS: only RIFLE_, ETag exposed, Rifle-* and Content-Digest headers allowed, never credentials
(there are no cookies anywhere, so there is nothing to forge cross-site).