hraness

content addressing for browser apps: dedupe, verify, sync

identity that comes from the bytes, not the row

Drafted by an AI agent at Ben Guo's direct request from the Hraness source repositories, and checked against those sources before publication.

A document in a browser app usually gets its identity from wherever it was saved: a row ID, a filename, a URL slug. That identity is assigned, not derived, and it breaks the moment the same bytes appear twice, a file moves, or a second device asks whether it already has this document. Content addressing inverts the relationship: the identity is a digest of the canonical bytes, so equality, integrity, and sync all reduce to the same question: do the bytes match?

The question this article answers: how does a local-first app deduplicate, verify, and synchronize documents without a server assigning identities? The working example is Soundfish, a browser music editor whose documents are carried in URL fragments and synced through a content-addressed library, with the same pattern showing up in Wordcell’s capture manifests, Oh’s snapshot digests, and soulscrape’s publication receipts.

the URL is the document

A Soundfish loop does not live behind a saved file. It lives in the link. encodeLoopFragment in lib/protocol/url.ts encodes a document as canonical CBOR, hashes it with SHA-256, optionally compresses with raw DEFLATE (chosen only when the compressed payload is strictly shorter) and emits one self-contained envelope:

#s1.<music-cid>.<codec>.<payload>.<transport-sha256>

Everything a reader needs is in the fragment: the codec tells it how to inflate, the payload is the document, and the trailing digest lets it verify that the bytes it decoded are the bytes the author hashed. The whole envelope is capped at MAX_LOOP_FRAGMENT_CHARS (32 KiB), so a share link stays a link. Copying the URL copies the document; there is no export format to drift away from the canonical one.

The discipline underneath is canonicalization. The digest names canonical bytes, not whatever the editor happened to hold in memory. Notes, lineage sources, and automation points are canonicalized before encoding, and the parser strips fields at their defaults so a semantically identical document always produces identical bytes. Content addressing only works when “the same document” is a byte-level fact rather than a judgment call.

one document, three names

The interesting design decision in soundfish.document/v1 is that it refuses to give one identifier three jobs. The library guide spells out the separation:

Identity Names Changes when
loop_… library ID one mutable project never: it is the stable thing edits attach to
MusicCID authored musical meaning the notes, production, effects, or automation change
transport digest one immutable canonical document any canonical byte changes, including titles
inst_… installation ID a device installation each install

The split matters because the three questions a system asks are different. “Is this the same song?” is a MusicCID question. The musical meaning survives a rename because the CID excludes composition and track titles, IDs, lineage, operation IDs, display order, and runtime sound assets while including notes, track production, shared effects, and all automation. “Do I already have these bytes?” is a transport-digest question. It covers the entire canonical document, title included. “Which project did this edit belong to?” is a library-ID question, because identical canonical bytes can belong to two distinct logical loops. And the installation ID answers “which device,” which is why the guide insists it is never treated as authentication or authorship.

Collapsing these into one ID is how sync systems end up either deduplicating two people’s different documents into one, or refusing to deduplicate the same document saved twice.

dedupe and repair

lib/library/local-songs.ts is a small IndexedDB store for saved arrangements, and its primary key is the transport digest rather than an ArrangementCID or a row counter. The consequences fall out of the key choice:

  • Saving the same canonical document twice adds no row: the digest already exists.
  • Title and section-label variants stay distinct, because they are different canonical bytes and therefore different digests.
  • A save under an existing digest doubles as a repair path: if the stored payload under that digest was damaged, rewriting it restores the correct bytes rather than duplicating the song.
  • On reload, the stored fragment, digest, and title are re-verified; a corrupted record surfaces as a typed failure instead of silently loading.

The list stays bounded at MAX_LOCAL_SONGS = 256, and corruption or quota failures preserve stored data rather than dropping rows to make the error go away. Content addressing makes deduplication free, but the store still has to decide, explicitly, what a repeated digest means. Here it means “identical bytes already retained,” and a repair, not a conflict.

verify, do not trust

Once identity is a digest, “did I get what was captured” becomes a checkable question everywhere in the stack.

Wordcell’s capture bundles record it at three levels. A schema v4 capture.json names document.path, document.bytes, and document.sha256. The digest covers the exact credential-redacted, newline-terminated Markdown bytes Wordcell wrote, and nothing else. Each asset carries its own byte count and SHA-256. wordcell capture verify reports integrity without printing the document, --verify-assets hashes every listed file, and any mismatch exits with status 3. An absent listed asset is an integrity mismatch, not a reader crash. Bundles from older schema versions that lack the v4 document record report integrity as unavailable, which verify refuses to present as success.

Oh turns the same idea inward. The storage spec requires every applied migration to be stored and checked by the exact SHA-256 digest of its SQL bytes, and refuses to run when the same version arrives with different bytes. Its projection layer goes further: projectionSha256 binds the contract, snapshot, dataset, rule pack, query, engine, and resolved evaluation limits together, so a cached result is reusable only when every identity it depends on is unchanged; any change is kind: "full-rebuild", and V1 claims no incremental maintenance. Content addressing is how the cache answers “is this still the answer to the question I asked” without trusting a timestamp.

And at the publication edge, soulscrape’s person-index CLI is idempotent on the packet digest: republishing identical bytes is a no-op, changed bytes bump the revision. The digest is the idempotency key.

sync as checkpoint arithmetic

The reason content addressing matters for sync is that it makes “what did you last see” a precise question. A Soundfish cloud checkpoint names its expected head by digest plus up to MAX_SYNC_PARENTS (two) parent digests, and the schema requires the expected head to be among the parents. The server stores canonical document bytes once, keyed by transport digest alongside the verified MusicCID, and authorizes snapshots through an owner-specific version reference, so content is shared by digest while authority stays per-account. A checkpoint that names a head the server no longer holds gets conflict back with the current head, never a silent overwrite; a digest-keyed content store means the server can also answer “do I have this exact document” without comparing bytes twice.

Fetching is shaped the same way: head metadata, the mutable pointer, is fetched separately from immutable snapshots, which are imported lazily and only fast-forward a clean local head. The digests are the load-bearing structure; everything else is transport.

the honest limits

Three caveats carry the weight:

  1. Canonicalization is the hard part, and it is yours. A digest names bytes, and two documents that mean the same thing but serialize differently are different identities. Soundfish pays for its digest stability with a canonical CBOR encoding, omit-at-default invariants, and a reviewed protocol revision for any new field.
  2. A digest proves integrity, not truth. Soulscrape’s packet reference says it outright: source packets are untrusted evidence, and a digest proves integrity (that the bytes are the bytes that were captured), not that their claims are accurate, authorized, or current.
  3. Identity is not provenance. Identical canonical bytes can belong to distinct logical loops, and a matching fragment alone is not proof of logical-loop ownership; Soundfish reattaches reloads only through validated history ownership whose digest and revision still match the local head.

Used within those limits, content addressing replaces a whole category of coordination (assigned IDs, version counters, existence checks) with one question any device can answer locally: do the bytes match?

sources

  • sound.fish: lib/protocol/url.ts, lib/protocol/model.ts, lib/library/
  • wordcell: docs/capture.md
  • oh: spec/v1/storage.md, spec/v1/projection.md
  • soulscrape: source-packet and person-index contracts