Comet Note Snapshots
draft
Related drafts:
Summary
This draft defines the first concrete Comet sync kind inside the reserved sync range:
kind:42061This kind is for Comet note snapshot events.
This draft also defines Comet as a local-first note system:
- the canonical local object is the note record
- sync transmits encrypted full-note snapshots
- vector clocks determine whether snapshots are newer, older, or concurrent
- relay-visible
vctags carry the wire vector-clock state so relays can retain and bootstrap current snapshots more intelligently - bounded recent history is a product feature, not the permanent sync substrate
- retained local snapshots now back an explicit local note-history feature
Goals
- Define Comet’s first concrete sync kind
- Align the sync protocol with a local-first note model
- Support multi-device offline edits without silent overwrites
- Let Comet keep bounded recent history rather than unbounded snapshot ancestry
- Keep Comet-specific payload semantics separate from the generic sync-range draft
Non-Goals
- Defining other future Comet sync kinds
- Defining notebook or file payloads in this draft
- Requiring relays to inspect encrypted payloads to determine current state
Kind Allocation
Comet note snapshots use:
kind:42061This kind is inside the reserved sync range:
40000 <= kind < 50000Local Canonical State
Comet should keep the local note record as the canonical object.
That means:
- the canonical local object is the note row
dis the stable document identifier for that note- note content remains plaintext locally
- vector-clock state is attached to the local note
- sync events are emitted from local note state rather than treated as the only source of truth
The intended local canonical fields are:
dmarkdownnote_created_atedited_atarchived_atpinned_atreadonlyvector_clocklast_edit_device_id
Current note state is authoritative locally.
Encrypted sync events are the transport used to move that state across devices and relays.
Current Comet Defaults
Current implementation defaults:
- local keeps the canonical note row as current state
- local keeps all unresolved conflict snapshots
- local keeps the current tombstone while a note remains deleted
- local keeps the last
10additional dominated snapshots per note - relay keeps all nondominated current snapshots plus enough dominated snapshots to reach a total retained payload window of
4snapshots per document when possible - local note history is derived from the retained local snapshot set
Sync Metadata
kind:42061 uses the sync metadata defined by the causal snapshot sync range draft:
- required:
d,o, repeatablevc - optional:
c
For Comet note snapshots:
dshould be a randomly generated UUIDv4dshould be serialized in canonical uppercase hyphenated formcshould normally benoteswhen presentvcshould be emitted once per vector-clock entry as["vc", "<device_id>", "<counter>"]
Example document coordinate:
B181093E-A1A3-492F-BF55-6E661BFEA397Device Identity
Each Comet device should have a stable device_id.
The device_id is used only for sync ordering and conflict detection.
It is not a document identifier.
For Comet, device_id should be an opaque randomly generated stable identifier, not a human-readable device label.
This is a deliberate privacy boundary:
- the relay needs stable device identifiers to compare
vcentries - those identifiers should not reveal a device name, platform, or hostname
- Comet should use opaque random identifiers such as UUIDv4 values, not labels like
MacBook-ProoriPhone
Vector Clock Model
Each note carries a vector clock.
Example:
{ "7F3B1C2A-91D4-4C8E-A0F1-52D1E7A0B9C3": 12, "91D4C8E0-4A1B-4B55-8E77-3C2048D623AF": 3}Clock update rule:
- when a device edits a note, it increments its own counter in that note’s vector clock
- then it emits a new encrypted snapshot event
Clock comparison rules:
- if local clock dominates remote clock, remote is stale
- if remote clock dominates local clock, remote should replace local
- if neither dominates, the snapshots are concurrent and the note is conflicted
This means Comet no longer needs a permanent explicit ancestry graph to decide whether one note state supersedes another.
For relay-facing metadata, the vector clock is carried in cleartext vc tags.
That allows a relay to:
- retain all nondominated current snapshots
- compact dominated snapshots more safely
- bootstrap current snapshots rather than a blind recent window
Clients should treat the vc tags as the wire source of truth for vector-clock state and hydrate local in-memory snapshot objects from those tags after decryption.
Encryption
For Comet implementation work, the kind:42061 payload should be encrypted using Comet’s current large-payload-capable NIP-44 variant.
This is a Comet implementation choice layered on top of the causal snapshot sync range.
If a standardized large-payload NIP-44 construction is adopted later, Comet should move to that standardized construction.
Wire payload encryption does not require local payload encryption.
Comet should keep canonical note text and canonical payload data unencrypted locally unless a separate local-at-rest encryption feature is introduced.
Canonical Payload Shape
The canonical payload for kind:42061 should be a JSON object with this shape:
{ "version": 1, "device_id": "7F3B1C2A-91D4-4C8E-A0F1-52D1E7A0B9C3", "markdown": "# Title\n\nBody", "note_created_at": 1712345678000, "edited_at": 1712345678000, "archived_at": null, "pinned_at": null, "readonly": false, "tags": ["work/project-alpha", "roadmap"], "attachments": [ { "plaintext_hash": "sha256-...", "ciphertext_hash": "sha256-...", "key": "hex..." } ]}Field guidance:
versionversions the Comet note payload formatdevice_ididentifies the device that produced this snapshotmarkdownis the canonical note bodynote_created_atis the document-level creation timestampedited_atis the last content-edit timestamparchived_atis present when the note is archivedpinned_atis present when the note is pinnedreadonlyrepresents user-intent readonly statetagscontains Comet note tagsattachmentscontains attachment references and decryption material
For tombstones:
- outer
oisdel - the event must still include at least one
vctag as required by the causal snapshot sync range - the payload must include
version,device_id, anddeleted_at - note body fields may be omitted or represented minimally
- the current tombstone should remain durable while the note stays deleted
Canonical JSON Rules
Before encryption, the payload should be serialized as canonical JSON.
Comet should use these rules:
- serialize the payload as UTF-8 JSON
- apply RFC 8785 JSON Canonicalization Scheme semantics to object serialization
version,device_id,tags, andattachmentsare always presentmarkdown,note_created_at, andedited_atare always present foro=putdeleted_atis always present foro=delarchived_atandpinned_atare omitted when absentreadonlyis omitted when false and included only when truetagsmust be canonicalized, deduplicated, and sorted lexicographicallyattachmentsmust be deduplicated byplaintext_hashand sorted lexicographically byplaintext_hashnote_created_at,edited_at,archived_at, andpinned_atare millisecond Unix timestampsmarkdownis preserved exactly as authored and is not normalized beyond normal JSON string escaping
These rules exist to make the snapshot payload deterministic before encryption and signing.
Title Semantics
Markdown is authoritative.
Comet note snapshots should not store title as separate canonical state in either sync metadata or the encrypted payload.
Instead, clients should derive title locally from markdown using this rule:
- scan markdown line by line
- ignore empty lines
- use the first non-empty H1 line beginning with
# - trim the heading text
- if no non-empty H1 exists, the derived title is the empty string
This means:
- changing the first H1 changes the derived title
- notes without an H1 have no canonical title
- title is a local projection, not part of canonical sync state
Fields Not In The Payload
The canonical encrypted payload should not include:
deleted_atbecause deletion is represented by sync metadatao=delvector_clockbecause vector-clock state is carried in cleartextvctagstitlebecause title should be derived locally from markdowntypebecausekind:42061already identifies the payload as a Comet note snapshot
Conflict Resolution
When two note snapshots are concurrent:
- Comet should mark the note conflicted and read-only
- Comet should surface both note states to the user
- user resolution should produce a new merged snapshot
- the merged snapshot’s vector clock should be:
- the pointwise max of both clocks
- then incremented for the resolving device
Example:
- left:
{ "A": 5, "B": 2 } - right:
{ "A": 4, "B": 3 } - merge on device
A=>{ "A": 6, "B": 3 }
The merged snapshot then dominates both prior snapshots.
Local History
Comet should treat retained local snapshots as a user-facing history feature, not only as internal sync state.
That means:
- retained local snapshots may be listed in the UI as note history
- a user may inspect an older retained snapshot without changing current state
- restoring an older retained snapshot should produce a new current note state
- restoring history does not rewrite history in place; it produces a newer snapshot from the restored content
Retention Direction
The intended retention direction is:
- keep the canonical local note record
- keep current snapshots needed for sync correctness
- keep a bounded recent snapshot history per note
- keep all unresolved concurrent snapshots until resolved
- allow older dominated snapshots to be dropped locally and on relays
Current Comet defaults:
- local keeps current materialized state
- local keeps all unresolved conflict snapshots
- local keeps the current tombstone for deleted notes
- local keeps the last
10additional dominated snapshots per note - relay keeps all nondominated current snapshots plus enough dominated snapshots to reach a total retained payload window of
4snapshots per document when possible
This gives Comet:
- explicit conflict handling
- bounded storage
- local-first semantics
- sync that does not require unbounded ancestry metadata
Relay Expectations
The first Comet profile should work with a relay that remains blind to note content but can inspect relay-visible vector-clock metadata.
That means:
- the relay stores and replays encrypted note snapshots
- the relay does not need to decrypt payloads
- the relay may compare cleartext
vctags to determine nondominated current snapshots - the client remains the final authority for local materialization after decrypting the profile payload and applying the relay-visible
vcclock
This keeps the relay blind to note content while still allowing better retention and bootstrap decisions.
That also means the relay is causality-aware rather than fully metadata-blind.
Observers can still learn:
- how many distinct device ids appear in a note’s history
- which device id produced the most recent visible increment
- rough relative edit activity per device id over time
Using opaque random device_id values reduces this leakage significantly, but it does not eliminate it entirely.
Current Direction
The intended direction is:
- canonical local note record
- encrypted snapshot transport
- vector-clock supersedence
- bounded recent history
- explicit user-visible conflict resolution
Future Work
- Implement vector-clock note sync locally
- Define any additional Comet sync kinds if notebooks or files need distinct profiles
Implementation Assumption
Comet should treat this transition as a clean break.
Implementation may replace the older graph-shaped sync storage model rather than preserving compatibility with older transition layers.
Implementation Plan
Recommended implementation order:
- keep the local
notestable canonical and adddevice_idplusvector_clock - make
kind:42061emit encrypted full-note snapshots from local note state - compare vector clocks locally during bootstrap and replay
- retain current snapshots plus a bounded recent-history window
- keep unresolved concurrent snapshots until the user resolves them
- move any remaining graph-specific code and schema out of the sync path