Skip to content

Schema Rules and Open Questions

Capsule schemas evolve over time, but the rules of evolution are fixed — what fields a writer may add, what a receiver may safely ignore, what fields are closed enums, and what timing/grammar rules apply. Each schema’s owner doc defines its fields; this doc defines what evolution is allowed across them. Schema-rule enforcement lives in capsule-core::crypto (sidecar/manifest decode) and the validation layers of every API crate.

Deny-by-Default for Unknown Request Fields

Section titled “Deny-by-Default for Unknown Request Fields”

Postel’s Law — as tightened in principles — applies asymmetrically:

  • In requests (client → server, or peer → server): unknown fields at known positions in a known schema are accepted and preserved verbatim (an unknown CBOR key inside a known manifest). Unknown fields at the top level that the receiver does not declare are rejected (a stray key at the request root). Schema-bearing requests that announce a sidecar_schema or crypto_suite_id the receiver does not implement are rejected outright. The asymmetry is deliberate: liberal acceptance in requests is what lets new clients write extensions, but only inside a known schema envelope.
  • In responses (server → client): unknown fields are preserved verbatim. A new server sending an old client a response with a new field does not break the old client.

Every enum in a signed or validated structure is closed per protocol_version — a value outside the set known at that version is a structural error, never a “future value to ignore.” This is a blanket rule, not a curated list, so it cannot rot: adding a value to any such enum bumps protocol_version (see Versioning — Album Protocol Version Pinning), and a pinned old album never sees the new value. It is enforced on both sides — the server’s structural envelope check (invariant 16) and the client’s verify_asset/decode path (see Validation).

The authoritative value set for each enum lives in its owner doc — AssetManifest.action in Authorization, content_type and gps.source in Metadata, DerivativeManifest.role in Provenance — never duplicated here.

All timestamp and ts fields are RFC 3339 strings, self-asserted and audit-only — never load-bearing for authorization or ordering, which ride the provenance chain and the MLS epoch (Keys — Write Authorization). The server records its own trusted received_at for any time-based policy.

A server-side sanity bound (default ±30 days of server wall-clock, deployment-configurable) is applied to writes only: a gross-drift guard that surfaces an honest client with a faulty NTP rather than silently distorting its audit trail. It is explicitly not a security control. Reads serve whatever timestamp was historically recorded.

Every field has a maximum length declared in the schema (e.g. caption_lww.value ≤ 4096 bytes; superseded_captions ≤ 16 entries). The receiver rejects an oversized value. No field is unbounded.

This is not a standalone contract — each entry is the negative of a rule owned by another doc, consolidated here only as an index. The defense never depends on clients honoring the list: the receiving server and the client-side verify_asset chokepoint reject the consequence structurally regardless (that is the entire point of refuse-by-default validation). A client that does any of these is buggy by definition, and the prohibition is enforced where its rule lives:

A correct client never…Enforced / owned by
Re-signs a manifest under a lower crypto_suite_id (downgrade)Primitives — Versioning
Signs for an album epoch it does not hold the write-tier key forKeys — Write Authorization
Issues an OR-set remove for an add_id it never observedMetadata — Add-id Binding
Strips _unknown or superseded_captions on write-backMetadata — the signature covers them
Overwrites or truncates a provenance chainProvenance — Append-Only
Submits revoke_all_sessions without master-key proofAuthentication
Decodes non-home-peer bytes outside the sandboxClients — Sandboxed Decoder
Silently promotes an AI tag to a user tag (must be a signed op)Metadata — Tag Provenance
Retries a 429 / 409 / 426 with the same payloadback off / re-align / upgrade first — Validation

Dropping a protocol_version from the server’s accepted window is a breaking change. The policy:

  1. Announcement. A deprecation cutoff date is published at /.well-known/capsule/deprecation ahead of the cutoff by at least the announcement window (default 90 days, deployment-configurable). The announcement names the cutoff date and the minimum protocol_version that will remain accepted.
  2. Server response. Below the cutoff, every response carries X-Capsule-Min-Client-Build and a Warning: header pointing to the deprecation URL.
  3. Hard cutoff. On the cutoff date, the dropped version moves outside [Min, Max]. Writes from clients pinned to that version receive 426. Reads still succeed.
  4. Stranded user. A user whose only client is below the cutoff still has every recovery path from Cryptography — Failure Modes: master key, cross-device, OGK, backup artifact. The deprecation does not strand data; it strands a specific old binary.

The deprecation surface is never retroactive against historical state. Old albums pinned to a dropped version remain readable forever — they just cannot be written to from a current client.

One design question remains open — and it is deliberately deferred to v2, not a v1 blocker:

  1. Cross-server album replication (v2). v1 pins each album to a single home server; v2 will need a story for cross-server MLS state and federated commit ordering.

The following questions have since been resolved and now live in their owner docs, not here: