Snapshot Changes Feed
draft
Related drafts:
Summary
This draft defines a relay-local changes feed for causal snapshot sync events.
The feed is responsible for:
- snapshot bootstrap against a stable relay snapshot
- ordered incremental replay
- reconnect after temporary disconnect
- live follow after catch-up
- relay-local checkpointing
This draft defines both:
- bootstrap of retained current snapshots
- relay-tail replay and live follow after bootstrap
Goals
- Efficient current-state bootstrap for local-first clients
- Ordered incremental sync for sync-range events
- Live updates after catch-up
- Relay-local checkpoints
- Compatibility with encrypted snapshot events
- Clear distinction between logical deletion and hard relay-side removal
Non-Goals
- Full-history anti-entropy
- Requiring relays to decrypt payloads or derive content-level conflict winners
- Defining application-specific payload semantics
- Defining conflict winner semantics
- Defining cross-relay comparable sequence numbers
Core Principle
The changes feed streams accepted sync events in relay sequence order.
For sync-range events:
- event kind identifies the sync protocol family
- sync metadata identifies document scope and operation
- relay sequence identifies local acceptance order on one relay
That means a client can:
- load the retained snapshot set at a stable relay snapshot
- resume from a saved relay-local cursor
- apply newly accepted sync snapshots in order
- keep one checkpoint per relay
Message Shape
Client request
All requests use the same message family:
["CHANGES", "<subscription-id>", <filter>]Relay status
[ "CHANGES", "<subscription-id>", "STATUS", { "mode": "bootstrap", "snapshot_seq": 12345 }]Relay bootstrap snapshot event
["CHANGES", "<subscription-id>", "SNAPSHOT", <event>]Relay replay or live-follow event
["CHANGES", "<subscription-id>", "EVENT", <seq>, <event>]Relay end-of-stream marker
["CHANGES", "<subscription-id>", "EOSE", <last_seq>]Relay error
["CHANGES", "<subscription-id>", "ERR", "<message>"]Filter Shape
Bootstrap and changes filters should support:
modesinceuntil_seqlimitkindsauthors#<tag>live
Recommended meaning:
mode:"bootstrap"or"tail"since: relay-local sequence cursor; return entries withseq > sinceuntil_seq: upper inclusive relay-local sequence boundlimit: maximum number of replayed entrieskinds: explicit sync kinds to includeauthors: sync namespaces to include#<tag>: filter on sync metadata tagslive: keep the subscription open for future accepted events after replay finishes
Sync-Range Constraints
This feed is intended for causal snapshot sync events.
Recommended rules:
modeis requiredkindsis requiredauthorsis requiredauthorsmust contain exactly one author pubkey- each requested kind must be inside the reserved sync range
- clients should use metadata filters such as
#d,#o, and#conly with causal snapshot sync kinds
Typical replay filters:
{ "mode": "tail", "since": 0, "kinds": ["<sync-kind>"], "authors": ["<pubkey>"]}{ "mode": "tail", "since": 2500, "kinds": ["<sync-kind>"], "authors": ["<pubkey>"], "#d": ["<document-coord>"], "live": true}For bootstrap:
sinceanduntil_seqare not requiredmodeshould be"bootstrap"authorsmust contain exactly one author pubkey- relays should interpret the filter as a retained-current-snapshot query at one stable snapshot
Bootstrap Semantics
Bootstrap is snapshot-oriented.
When a relay accepts a CHANGES request with mode = "bootstrap", it should:
- validate the filter as a causal snapshot sync bootstrap filter
- capture
snapshot_seq - resolve the retained, fetchable, nondominated current sync snapshots matching the filter at that snapshot
- send
STATUSwithsnapshot_seq - stream one
SNAPSHOTevent per retained current matching snapshot - send
EOSEwithlast_seq = snapshot_seq
Important semantics:
- the returned events are the retained fetchable nondominated current snapshots at
snapshot_seq - events accepted after
snapshot_seqare outside the bootstrap snapshot - because this sync family exposes relay-visible causal metadata, a relay should use that metadata to determine nondominated current snapshots
- retained dominated history is not part of bootstrap output by default
- bootstrap
SNAPSHOTevents should be emitted in deterministic order, using ascendingcreated_atand then ascending event id as the recommended tie-breaker - the client decrypts the returned snapshots, hydrates profile state, and compares vector clocks locally before applying
Bootstrap does not attempt full-history reconciliation.
Bootstrap is retained-current-snapshot-only in this version of the draft.
Clients may materialize a bounded local note-history feature from retained snapshots, but that history is a client feature layered on top of bootstrap and replay rather than a distinct transport concept.
Sequence Model
seq values are relay-local.
That means:
- the client must keep one cursor per relay
- two relays may assign different
seqvalues to the same sync event - cross-relay convergence happens through note identity plus decrypted payload metadata, not through
seq
The feed is ordered by relay acceptance, not by created_at.
That distinction matters:
created_atbelongs to the eventseqbelongs to the relay- reconnect and replay use
seq, not event timestamps
EOSE And Checkpoints
After replaying the requested range, or after completing bootstrap snapshot delivery, the relay should send:
["CHANGES", "<subscription-id>", "EOSE", <last_seq>]Clients should persist last_seq as the relay-local checkpoint.
If live is true, the relay keeps the subscription open after EOSE and continues emitting future accepted sync events for the subscription filter.
Bootstrap Handoff Into Tail Replay
The purpose of bootstrap snapshot_seq is to avoid a race window between bootstrap snapshot loading and relay-tail replay.
Recommended client flow:
- open
CHANGESwithmode = "bootstrap" - receive
snapshot_seq = S - load and decrypt all returned bootstrap events
- compare their payload metadata, such as vector clocks, with local note state
- apply remote snapshots that dominate local state
- surface concurrent snapshots as conflicts
- upload missing or newly merged local snapshots only after conflict/policy evaluation
- start
CHANGESwithmode = "tail",since = S, andlive = true - continue from the relay tail
Bootstrap is concerned with current and retained snapshot transport only.
- it is not a full-history protocol
- it is not a local history UI protocol
- clients may keep a bounded local history window from retained snapshots after apply
This gives the protocols clear responsibilities:
CHANGESin bootstrap mode answers “what retained current sync snapshots existed at snapshotS?”CHANGESin tail mode answers “what happened after snapshotS?”
Logical Delete vs Hard Delete
Logical document deletion must flow through a normal sync event:
["o", "del"]That is protocol-level deletion.
Clients maintaining logical state should treat o=del as authoritative for document deletion.
Client Apply Rules
When a client receives a sync event through bootstrap or the changes feed, it should:
- validate that the event kind is inside the reserved sync range
- validate required sync metadata
- decrypt the payload for concrete profiles that require local comparison
- compare the incoming snapshot against local state using profile-defined ordering metadata such as vector clocks
- apply dominating snapshots
- ignore stale dominated snapshots
- treat nondominated concurrent snapshots as real conflicts until application logic resolves them
The changes feed does not define any built-in winner among conflicting snapshots.
Recommended client behavior for unresolved concurrent state:
- make the document read-only
- require explicit user resolution before further editing continues on that document
Relay Expectations
For the simple local-first model, the relay should:
- store and replay encrypted sync snapshots
- filter by relay-visible sync metadata only
- maintain relay-local
seqordering - avoid deriving note content from encrypted payloads
- use relay-visible causal metadata to retain and bootstrap nondominated current snapshots
This keeps the relay simple and lets the client verify vector clocks after decryption.
Retention And Compaction
This model supports payload compaction and replay retention limits, but this draft does not fully define them.
Important compatibility point:
- relay-local
seqreplay is only meaningful inside the relay’s retained history window
Relays should advertise replay retention through relay info metadata.
Richer snapshot-retention metadata may also be advertised separately as described in Snapshot Retention And Compaction.
Minimum required field:
{ "changes_feed": { "min_seq": 125000 }}Field meaning:
min_seq: earliest replayable relay-local sequence on this relay
If a client has fallen behind the retained replay window, the client should fall back to bootstrap rather than assume the feed alone can restore full state.
Recommended client rule:
- if a saved relay checkpoint is less than
min_seq, replay alone is insufficient - the client should bootstrap first, then resume tail replay from the returned
snapshot_seq
Snapshot bootstrap is intentionally more compaction-friendly than full-history repair.
Open Questions
- Should the feed later expose metadata-only replay modes in addition to full events?