DFOS Content Model
Standard content schemas for documents committed to DFOS content chains. JSON Schema (draft 2020-12) definitions for content objects committed by CID. These schemas are the vocabulary DFOS uses internally and the starting vocabulary for applications built on the protocol.
Protocol Specification · schemas.dfos.com · Source
Encoding
A content object is committed to a content chain by CID, derived from the canonical dag-cbor encoding of the object itself:
documentCID = CID(dagCborCanonicalEncode(contentObject))The protocol commits to content by hash and never inspects what is inside, with one constraint on the encoding: every number in a content object MUST be an integer in JSON's safe range ([-(2^53 - 1), 2^53 - 1]), with no fractions, no NaN, and no ±Infinity. Encode fractional or larger-magnitude values as strings. This keeps the content CID byte-identical across implementations (see Number Encoding in the protocol spec).
Every document in this vocabulary carries a $schema property naming its content type.
{
"$schema": "https://schemas.dfos.com/post/v1",
"format": "short-post",
"body": "Hello world."
}Because $schema is part of the content object, it is behind the documentCID and is cryptographically committed in the content chain. A verifier resolves the document, reads $schema, and validates against that schema. Documents are self-describing. $schema is a convention of this vocabulary rather than a protocol rule: an operation committing a document without one is a valid operation, and this vocabulary does not describe that document.
Schema Versions
Schemas are versioned in the URI path (/post/v1, /post/v2).
- Additive within a version. New optional fields are added to a published version at any time, and documents already committed against that version stay valid.
- Breaking changes take a new version. Removing a field, changing a type, or adding a required field is a new version URI, never an in-place edit.
- Implementations declare which versions they understand. An application accepts
post/v1andpost/v2at once, or onlypost/v1.
Two immutabilities, deliberately distinct. A committed document is CID-addressed and byte-immutable: a specific document never changes, and an edit is a new document with a new CID appended to the content chain. A published schema version evolves only additively: adding optional fields to post/v1 never invalidates documents already committed against it.
A document's own field values are fixed at the operation that set them.
Documents
Schema files live in schemas/ in the protocol package. Each is a standalone JSON Schema (draft 2020-12) definition, served at https://schemas.dfos.com.
Post (https://schemas.dfos.com/post/v1)
The primary content type. Covers short posts and long-form posts via the format discriminator. Comments and replies are not post/v1 surface: threaded content needs signed target linkage (see Intra-chain references) and takes its own schema rather than additional format values.
| Field | Type | Required | Description |
|---|---|---|---|
$schema |
string | yes | "https://schemas.dfos.com/post/v1" |
format |
enum | yes | "short-post" or "long-post". Set at creation and not changed by later revisions |
publishedAt |
string | no | Asserted original publication time, an RFC 3339 date-time string per the published schema's format. See Two clocks |
title |
string | no | Post title, typically for long-post format |
body |
string | no | Post body content, markdown (CommonMark) text |
cover |
media | no | Cover image as a media object |
attachments |
media[] | no | Attached media as media objects |
credits |
credit[] | no | Ordered authorship credits. See Credits |
post/v1 is a closed shape (additionalProperties: false).
Two clocks. A post and its operations carry two distinct times. The operation's createdAt records when the operation was signed and is protocol state. publishedAt records when the content was originally published, as asserted inside the signed document: a chain anchored long after the fact carries the original publication time here while its genesis operation records the later anchoring time. publishedAt is assertion-tier, like credits: the protocol verifies the signer, never the claim. A later revision MAY change it, and the operation log preserves every previously committed value, so re-dating is auditable. Operation createdAt is never backdated to encode publication time.
No topics. post/v1 defines no topic or category field. Topic labels are mutable organizational taxonomy, and a signed CID-committed document that embedded them would turn every taxonomy rename into a corpus-wide revision wave. Organization lives in index entries and host-side projections, which are mutable and cheap to rebuild.
Body to attachment binding. A post body MAY embed attachment://<id> refs inline, for example an image reference inside markdown. Each inline ref SHOULD have a corresponding entry in attachments whose uri is that same ref. The body names media; attachments carries the verifiable reference.
The example post documents in the protocol spec's test-vector section, and the matching examples/ chain fixtures, carry an older draft credit shape and do not validate against post/v1. Those bytes are the input to published hashes, and a document CID from them appears verbatim in the spec's reference-artifact tables, so they are read as fixtures for the signing and verification rules they demonstrate, never as content-model examples. A drift guard in the protocol test suite pins the fixtures and the spec vectors to each other.
Profile (https://schemas.dfos.com/profile/v1)
The displayable identity for any agent, person, group, or space.
| Field | Type | Required | Description |
|---|---|---|---|
$schema |
string | yes | "https://schemas.dfos.com/profile/v1" |
name |
string | no | Display name |
description |
string | no | Short bio or description |
avatar |
media | no | Avatar image as a media object |
links |
link[] | no | External links, up to 20 { uri, label?, description? } items |
{
"$schema": "https://schemas.dfos.com/profile/v1",
"name": "Alice",
"avatar": {
"uri": "attachment://media_abc123",
"cid": "bafkreibovzpnn2y6dquvxhidhx64hg7smduemox7drjs4vprjhlbmivfli"
}
}The credited party's profile is authoritative for its current display name.
Index (https://schemas.dfos.com/index/v1)
An index chain is a curated map of content refs: a space's catalog, an author's works, a reading list, a set of pinned items. It is an LWW-Map folded via the canonical fold. Each operation commits an index/v1 document carrying deltas, and the resolved index is the fold over the operations in the log.
An index document carries an array of deltas. A single append sets or removes several entries at once, and the index accumulates through many small delta documents instead of re-committing a whole catalog each time. The deltas live in the document blob, which the operation-size cap does not measure: content operations commit only the documentCID. The protocol does not bound document blob size; any blob limit is gateway or application policy.
| Field | Type | Required | Description |
|---|---|---|---|
$schema |
string | yes | "https://schemas.dfos.com/index/v1" |
deltas |
delta[] | yes | Ordered deltas contributed by this operation |
Each delta is one of two shapes:
| Delta | Effect |
|---|---|
{ "op": "set", "key", "value"? } |
Add or replace entry key. value is optional metadata; omit it (or use {}) for a pure set-membership entry |
{ "op": "remove", "key" } |
Drop entry key |
keyis a content ref, a 31-char content chain id or a CID, consistent with how refs are named elsewhere in this model.valueis an optional entry-metadata object{ label?, order?, … }.labelis a display string;orderis an integer ordering hint (integers only, per the encoding rule above). A pure set-membership index uses the degeneratevalue: {}. Unknown metadata fields are preserved.
{
"$schema": "https://schemas.dfos.com/index/v1",
"deltas": [
{
"op": "set",
"key": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"value": { "label": "First Release", "order": 1 }
},
{ "op": "remove", "key": "ccccccccccccccccccccccccccccccc" }
]
}Fold semantics. The resolved index is the canonical fold as an LWW-Map:
- Linearize the operations in the log, all branches, into the fold order.
- Flatten each
index/v1document'sdeltasarray in array order, producing one ordered delta stream. - Fold the stream:
setwriteskey → value,removedeleteskey. The last delta touching a key wins at its linearized position, so aremovesupersedes an earlierset, and a latersetre-adds a removed key.
Unknown delta shapes are skipped deterministically. A delta whose op is neither set nor remove, whose key is not a string, or whose set value is present but not an object is ignored, not an error. Every reader skips the same deltas, so the vocabulary grows without forking existing readers. The published JSON Schema mirrors this: schema validity covers the known vocabulary constraints only (a delta needs an object shape and a string op; a set or remove carries a string key), and validators MUST NOT reject documents carrying additional delta shapes.
Because the fold is branch-inclusive and last-applied-wins, an index converges: any ingest order of the same operation set folds to the same map, and two clients that concurrently append entries both keep their writes. If the chain's selected head is delete-terminal, the index is deleted and the fold is moot (see Delete-terminality).
The index/v1 fold is foldIndexV1(ops) in @metalabel/dfos-protocol/fold. See schemas/index.v1.json and the worked chain in examples/index/.
Media object
The standard representation of a reference to external media bytes. Defined once here; schemas that carry media reference this shape (the profile/v1 avatar field, and the post/v1 cover and attachments fields).
{
"uri": "attachment://media_abc123",
"cid": "bafkreibovzpnn2y6dquvxhidhx64hg7smduemox7drjs4vprjhlbmivfli",
"href": "https://cdn.example.com/media/abc123.jpg"
}| Field | Type | Required | Description |
|---|---|---|---|
uri |
string | yes | Canonical reference to the media: an attachment://<id> ref or any other URI. Always present |
cid |
string | no | Content commitment: CIDv1, raw codec (0x55), sha2-256, base32 lowercase, over the media bytes |
href |
string | no | Resolution hint: a plain URL where the bytes may currently be fetched. Non-normative |
uri(REQUIRED) is the stable, canonical name of the media. It is anattachment://<id>ref or any other URI scheme (ipfs://,https://, …). Theuriidentifies; it does not promise integrity.cid(OPTIONAL) is a verifiable commitment to the bytes: a CIDv1 with the raw codec (0x55) and sha2-256, encoded base32 lowercase (a 59-charbafkrei…string), computed over the media bytes exactly as stored and served. Media bytes are opaque binary, so a consumer verifies by hashing the fetched bytes directly; unlike document blobs, no re-canonicalization is involved.cidis optional because some media has no cid computed for it. Whencidis present, a consumer SHOULD verify the bytes it receives against it.href(OPTIONAL) is an implementation-dependent fallback: a plain URL where the bytes may currently be fetched. It is non-normative, carries no integrity promise, and MAY rot. Consumers resolveuriand verify withcid;hrefis a hint, never the reference.
The attachment:// ref is opaque and host-scoped. <id> is an identifier meaningful to the host that committed the document, and nothing about the bytes is derivable from the ref itself. Resolution, turning the ref into fetchable bytes, is host- or gateway-dependent: a content-plane host resolves it out of protocol, for example via a signed-CDN API. The ref carries no integrity. cid is the only integrity commitment a media object makes, and a media object with no cid gives a consumer nothing to check the bytes against.
A media object is the referential case: a document is either terminal, where the { $schema, … } blob is the content, or referential, where it describes how to fetch external bytes. Resolving a media object, the delivery of the actual media bytes, is outside the protocol. The content plane serves the document that contains the media object as opaque bytes and never dereferences the pointer. There is no media server: media lives at the application and delivery layer, bound to the proof plane only by the signed reference, with cid as the optional content hash that lets a consumer verify the bytes it receives.
Credits
Who signed a thing and who made it are different claims. The protocol verifies signers: the kid DID in every operation's JWS header is cryptographic fact. Authorship is a document-layer statement, and this vocabulary expresses it in two tiers.
A credits[] entry is the assertion tier: the identity that signed the revision asserts these credits, and nothing more is claimed. Consumers SHOULD display it as an assertion of the signer, not a verified fact. This is often exactly right, because a custodial host or space signs content on behalf of the people it credits. An entry that stops there is unclaimed, the ordinary case.
A credited DID upgrades its credit to the proof tier by signing the credit itself: a credit claim, a small standalone JWS asserting (contentId, did, role), carried in the entry's claim field. Both the entry shape and the claim payload are published as credit-claim/v1.
Claims travel inside document bytes. A claim is not an operation and is not gossiped. Relays are not credit-claim aware: a relay sees document blobs, stores and serves them as opaque bytes, and applies exactly the access control it already applies to those bytes. A credit on gated content is therefore exactly as visible as that content, and attribution never becomes more public than the work it attributes. The verifier is always the consumer that reads the document.
The credits[] entry
"credits": [
{
"did": "did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr",
"role": "photography",
"name": "Alice",
"claim": "eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmNyZWRpdC1jbGFpbSIsImtpZCI6..."
},
{ "did": "did:dfos:nzkf838efr424433rn2rzkdv8h7t9ae", "role": "author" }
]| Field | Type | Required | Description |
|---|---|---|---|
did |
string | yes | The credited identity |
role |
string | no | The credited role, open vocabulary, byte-exact. REQUIRED when claim is present |
name |
string | no | Display name at time of publication, a convenience, never authoritative |
claim |
string | no | A did:dfos:credit-claim JWS binding this entry |
- Array order is display order, and the first entry is the primary author.
nameis a cached display string, not a source of truth. It records what the document said when it was signed, so a document renders without resolving every credited DID. A consumer that resolves profiles SHOULD prefer the profile name.nameis never compared during verification.roleis REQUIRED wheneverclaimis present, becauseroleis a component of the bind. The published schema encodes this asdependentRequired: { claim: ["role"] }, androlehasminLength: 1, so an empty role is malformed rather than a wildcard.- Omit
creditsentirely for unattributed content. An empty entry list and an absent list mean the same thing.
The entry is a closed shape (additionalProperties: false).
The credit claim envelope
A credit claim is a JWS in the same envelope family as credentials and revocations, with its own typ.
{
"alg": "EdDSA",
"typ": "did:dfos:credit-claim",
"kid": "did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr#key_r9ev34fvc23z999veaaft83nn29zvhe",
"cid": "bafyrei..."
}| Field | Value | Description |
|---|---|---|
alg |
"EdDSA" |
Ed25519 signature algorithm |
typ |
"did:dfos:credit-claim" |
Protocol-specific type discriminator |
kid |
DID URL | did:dfos:<id>#<keyId>, identifies the signing key |
cid |
CID string | Content address of the payload |
The kid MUST be a DID URL containing #, and its DID portion MUST equal the payload's did. This is the rule that makes a claim self-asserted: only the claimant can claim its own credit. A third party signing "Alice is the photographer" produces an invalid claim, not a weaker one; that statement already has a home in credits[], where it reads as the signer's assertion. Any key role signs a claim.
{
"version": 1,
"type": "credit-claim",
"contentId": "cv7n8vkvr64cctf3294h9k4eanhff8z",
"did": "did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr",
"role": "photography",
"createdAt": "2026-03-07T00:00:00.000Z"
}| Field | Type | Required | Description |
|---|---|---|---|
version |
1 |
yes | Schema version (literal 1) |
type |
"credit-claim" |
yes | Literal discriminator |
contentId |
string | yes | The 31-char content chain id this claim binds to, the binder |
did |
string | yes | The claimant DID. MUST equal the kid's DID |
role |
string | yes | The claimed role, open vocabulary, compared byte-exact |
createdAt |
string | yes | The timestamp grammar (millisecond precision, UTC) |
asOfDocumentCID |
CID | no | Optional pinned document state |
Unknown top-level fields are preserved and ignored, per the protocol's MUST-ignore-unknown rule. The CID commits to the exact bytes, so a verifier that stripped unknown keys would fail its own CID check.
role is an open vocabulary compared by exact, case-sensitive byte equality. "photography", "editor", "mixed by", and "翻訳" are all legal, and "Author" is a different role from "author". No normalization of any kind is applied: not case folding, not Unicode normalization, not whitespace trimming. Applications that want a controlled vocabulary enforce it at their own layer.
createdAt is provenance metadata. It records when the claimant signed. Verification never compares it against a clock, and a claim has no expiry. Signers MUST normalize createdAt to whole seconds, so the millisecond component of a signed claim is always 000. This binds signers, not verifiers: a verifier accepts any conforming millisecond-precision timestamp. A caller-supplied timestamp is normalized rather than passed through, so re-deriving a claim from the same inputs lands on the same CID on every implementation.
asOfDocumentCID is an optional stronger statement: I credit myself on the chain, and this is the document state I was looking at. It is not part of the bind, and consumers that ignore it are fully conformant. Omitting it is the default and is CID-neutral: an absent asOfDocumentCID encodes identically to a claim that never had the field. When the key is present it MUST be a non-empty string.
CID derivation is identical to every other protocol object:
dagCborCanonicalEncode(payload) -> SHA-256 -> CIDv1 (dag-cbor + SHA-256)The derived CID is embedded in the protected header as cid, and verification re-derives it from the parsed payload and compares. Mismatch is a verification failure. As everywhere else in the protocol, dag-cbor is used only for CID derivation; the JWS body carries JSON.
| Bound | Value | Applies to |
|---|---|---|
| credit claim JWS token | 4096 bytes | the serialized claim token |
Verifiers MUST reject a claim whose serialized JWS token exceeds 4096 bytes, checked before any decode. This is the claim's single aggregate byte arbiter: role and contentId carry no separate per-field length caps. The cap is tight because claims travel inside document bytes, and a document crediting twelve collaborators carries twelve tokens in its blob. This bound is validity-determining and MUST be identical across implementations.
The bind
The bind is the exact match of (contentId, did, role) between the entry plaintext and the signed claim payload:
| Component | Entry side | Claim side | Comparison |
|---|---|---|---|
contentId |
the chain hosting the document | payload.contentId |
exact string equality |
did |
entry.did |
payload.did |
exact string equality |
role |
entry.role |
payload.role |
exact, case-sensitive byte equality |
All three MUST match. Nothing is normalized, folded, or coerced on either side. A claim that verifies cryptographically but names a different role than the entry it sits in is a failed bind, and the entry is invalid.
An absent entry.role is a mismatch, not a wildcard. payload.role is always present and non-empty, so a claim-bearing entry without a role can never satisfy the third row, and verifiers MUST resolve it invalid. It is not unclaimed: a claim is present, so the entry is making a verifiable assertion and failing it.
What the bind establishes, when it holds, is narrow: the document's signer and the claimant independently assert the same credit. It does not establish that the credit is true. What it rules out is a signer attributing work to someone who never agreed to be attributed, and a claimant attaching itself to work no one credited it for. Neither party can produce a claimed entry alone.
The claim binds the chain's contentId, not a document CID and not the chain head CID. Binding the chain is what lets a claim survive every subsequent revision verbatim, and naming the chain in the signed bytes is what confines a claim to the one chain it was made about: a claim lifted into another chain's document fails the bind and resolves invalid.
Verification states
Every credits[] entry resolves to exactly one of four states. A consumer SHOULD surface the distinction; collapsing all four into "credited" throws away the mechanism.
| State | Condition | How to read it |
|---|---|---|
| claimed | claim present, verifies, and all three bind components match |
Signer and claimant both assert this credit |
| unclaimed | No claim field |
The signer asserts this credit; the claimant has not countersigned it |
| invalid | claim present but verification or the bind fails |
Something is wrong. Do not present this as either claimed or unclaimed |
| unverifiable | claim present and well-formed, but the claimant's identity chain cannot be resolved |
Verdict unknown. Insufficient information, not a failure |
unclaimed is legal and ordinary. It is the default for most content: the credited party may hold no keys, or may be a person credited by a host that signs on their behalf. A consumer MUST NOT render unclaimed as suspicious. The only thing an unclaimed entry lacks is the second signature.
invalid is different in kind and SHOULD be surfaced as such. Rendering it as unclaimed launders a failure into the ordinary case.
unverifiable is honest ignorance. A transport failure is unverifiable, never invalid: an outage is not evidence about a signature. Consumers SHOULD NOT cache an unverifiable verdict as invalid.
The two failure verdicts MUST be machine-distinguishable. A verifier that signals every failure as one undifferentiated error forces consumers to string-match diagnostic prose. Implementations MUST expose the verdict structurally: the reference implementations carry it as a field on a typed error (TypeScript: CreditClaimVerifyError.reason) and as errors.Is-able sentinels (Go: ErrCreditClaimInvalid and ErrCreditClaimUnverifiable). A failure whose cause cannot be attributed, such as an unexpected fault inside the verifier itself, MUST resolve to unverifiable.
Verification algorithm
To verify one credits[] entry, given the entry, the contentId of the chain whose document contains it, and a way to resolve identities:
- No
claimfield? The entry is unclaimed. Stop. - Size. If the
claimtoken exceeds 4096 bytes, the entry is invalid. Check this before any decode. - Decode the JWS. Failure to decode resolves invalid.
typ. MUST bedid:dfos:credit-claim. Anything else resolves invalid. Another envelope type from this family is not a credit claim, no matter how well it verifies.- Payload schema.
versionMUST be1,typeMUST becredit-claim,contentIdMUST be a 31-char content chain id,didMUST be non-empty and carry thedid:prefix,roleMUST be non-empty,createdAtMUST parse per the timestamp grammar, andasOfDocumentCID, if the key is PRESENT, MUST be a non-empty string. Otherwise invalid. Thedid:check is a prefix check, not fulldid:dfosvalidation: a claimant is not required to be adid:dfosidentifier. kidtodid. Thekid's DID portion MUST equalpayload.did. Otherwise invalid.- Resolve the claimant identity named by
payload.didand find the key named by thekidfragment among every key that identity chain has ever held. A claim has no basis time and no temporal check. Unresolvable identity resolves unverifiable. Resolvable identity with no such key resolves invalid. - Signature. Verify the JWS under that key. Failure resolves invalid.
- CID integrity. Re-derive the payload CID and compare against the header
cid. A missing or mismatchedcidresolves invalid. - The bind.
payload.contentIdMUST equal the hosting chain'scontentId;payload.didMUST equalentry.did;payload.roleMUST equalentry.rolebyte-for-byte. Any mismatch resolves invalid. - Otherwise the entry is claimed.
Step 10 is the step implementations skip. A claim that passes steps 1 to 9 is a valid signed statement by someone about something; verifying it without binding it to the entry that hosts it verifies the wrong proposition. An empty expected contentId MUST be an error, never a skip, because the empty string is what an unhydrated database column or a failed parse produces. Implementations SHOULD expose a single call that takes the whole entry plus the hosting contentId and returns one of the four states; the reference implementations do (verifyCreditEntry and VerifyCreditEntry), and it is the call consumers reach for.
An implementation MUST resolve claimant keys from the identity chain's full key history, not from its current state. A resolver that returns only current-state keys resolves invalid for every claim signed before the claimant's most recent rotation, which silently un-credits real work: a claim signed in 2026 does not evaporate because the claimant rotated keys in 2028.
The algorithm carries no expiry or freshness check, no revocation lookup, no relay query, and no identity-deletion gate. Verification MUST NOT consult the claimant's isDeleted state: a claim signed by an identity that has since been tombstoned still verifies, and its entry is still claimed. Attribution is history; authorization is standing. Deletion ends an identity's ability to act and does not rewrite what it did. A claim's verifiability depends on the claimant's identity chain remaining resolvable, which is not the same as the identity remaining live: a resolver that returns tombstoned identities with their keys lets old credits keep verifying, and one that returns nothing for a deleted DID yields unverifiable.
Claim stability and renunciation
A given (contentId, did, role) triple SHOULD be signed once and the resulting token reused verbatim wherever that credit appears. Re-signing produces a byte-different claim, which changes the embedding document's CID, which appends a chain operation. Store the token; do not re-mint it.
The did:dfos:credit-renounce type is reserved and undefined. There is no renunciation mechanism, and implementations MUST NOT emit or accept anything under that typ. What a claimant does without any new primitive is stop claiming: an entry whose claim is dropped on the next revision is unclaimed, an ordinary state, and the operation log preserves the earlier revision, so nothing is falsified.
A relay MAY serve a credits index as an optional profile: a (contentId, did, role) projection derived exclusively from the current head documents of publicly readable content, where rows are assertion-tier discovery hints that restate what a public document says, never authorization inputs and never a verdict on a claim's validity.
Chain Interpretation
A content chain is a signed append-only log. The protocol enforces ordering, authorship, and integrity. It does not prescribe what the chain means. How an application interprets a chain depends on the content types committed to it.
Living document
The chain represents a single evolving thing: a profile, a post, a policy document. Each operation is a revision. The resolved state is the latest documentCID, and history is audit trail. This is the default interpretation for the standard schemas. Edit lineage is tracked via baseDocumentCID on the content operation payload, so each new operation references the document CID it replaced.
Stream
The chain represents a sequence: a feed, a journal, a log. Each operation is a discrete emission, not a revision. There is no single current state; the chain is the content. The resolved content is the full ordered list of documents, and applications read a stream by walking the chain log and collecting each operation's documentCID.
Event fold
The chain represents a sequence of events that fold into a computed state. Each operation contributes a delta or event, and the resolved state is the result of replaying them in order. The $schema of the documents defines the event types and fold semantics. The chain log is the source of truth; the projected state is derived.
Projection per schema
| Schema | Projection |
|---|---|
post/v1 |
Living document. Head documentCID is the current post; history is edit trail |
profile/v1 |
Living document. Head documentCID is the current profile |
index/v1 |
Canonical fold. LWW-Map folded over the operations in the log, every branch |
Stream and event-fold schemas define their own projection rules in their schema documentation. The protocol does not enforce projections; these are reading conventions that applications agree on.
Intra-chain references (targetOperationCID)
Content documents reference specific operations within their own chain or other chains via targetOperationCID: a reply naming the operation it answers, a reaction naming the operation it reacts to, an annotation naming a specific version of another chain's content.
targetOperationCID is a content field, inside the document committed by CID, not an operation field. The protocol commits to it via documentCID and does not interpret or validate it. Applications resolve the reference by looking up the target operation on a relay.
The Canonical Fold
The event fold interpretation says a chain's resolved state is the result of replaying its operations in order. The canonical fold makes that order precise: a deterministic total order over the operations a reader holds, all branches rather than only the selected-head branch, so that any implementation holding the same set of operations computes the same folded state. Which operations you hold is a choice of relay.
Linearization
The fold order is the relay's head-selection comparator generalized from picking one tip to ordering the whole log. Head selection prefers, among the chain's tips, the operation with the highest createdAt, breaking ties by the highest operation CID, both compared byte-wise over the multibase CID string and the fixed-width ASCII timestamp-grammar string. It is a code-point comparison, never locale collation, so every implementation agrees.
The linearization lays that same preference out in full, ascending, so the operation head selection would prefer sorts last:
createdAtascending (byte-wise string comparison).- Operation CID ascending as tiebreak (byte-wise multibase string).
The two orderings are exact reverses, so they never disagree: the last operation of a full-log linearization is exactly the operation head selection picks. This holds structurally, because each write's createdAt exceeds its predecessor's, so the operation with the greatest createdAt is always a tip. Sorting the head-preferred operation last is what makes the fold last-applied-wins.
Head selection and the fold call the same exported comparison function, compareHeadPreference, so the two cannot drift.
Branch-inclusive
Folding every branch is a deliberate divergence from the head-selection reading used by living-document schemas. Head selection answers "which single document is current", and a losing fork is simply not the head. The fold answers "what is the merged state of this chain", where concurrent forks converge rather than compete, because dropping a branch would silently lose the writes on it. Both readings coexist on the same wire format: a register chain reads its head, a fold chain folds its whole log.
The fold operates on already-verified operations and applies no wall clock of its own. The future-timestamp bound is enforced at ingest by the relay, and a caller folding operations it verified itself carries that obligation.
Delete-terminality
The fold assumes a live chain. If the selected head branch is delete-terminal, meaning the highest-ranked tip is a delete, the chain is deleted, resolution reports it as such, and the fold is moot: a consumer checks isDeleted from chain verification first and does not fold a deleted chain. A delete on a non-head branch is another superseded operation and does not delete the chain. Per-branch delete semantics are a content-chain notion; identity chains answer deletion with the explicit restore operation instead.
Library
The fold is a set of pure functions over already-verified operations, published at @metalabel/dfos-protocol/fold, with no cryptographic or network dependencies:
linearize(ops)is the deterministic total order above.foldLwwMap(deltas)is the generic LWW-Map fold over an ordered delta stream.foldIndexV1(ops)is theindex/v1fold built on the two.
Custom Schemas
Any implementation defines custom document schemas following the same pattern: a JSON Schema with a $schema const field pointing to a unique URI. The protocol commits to the document via CID regardless of what is inside. The standard schemas are conventions, not constraints.
Custom schema URIs use a namespace you control, for example https://schemas.example.com/my-type/v1, to avoid collisions with the standard library.