Skip to content

Compatibility rules

The protocol's compatibility contract is short, stated in CLAUDE.md Rule 9 § "Coding standards":

Source: CLAUDE.md Rule 9. Quoted: "Additive-only schema evolution — never remove fields, only add optional ones."

This rule applies to:

  • accepts.<op>.schema and .returns
  • emits.<event>.schema
  • streams.<name>.schema
  • state.<key>.schema
  • The shape of MxEnvelope and the request/response/error payloads
  • HTTP gateway endpoint shapes (when added)

Why additive-only

A live network has many independent clients and providers running at different versions. Removing a field that an older caller still sets, or renaming an op, breaks them. With additive-only:

  • New optional fields are ignored by old code.
  • New ops are unused by old code, present for new code.
  • Default values fill in missing fields when old code talks to new servers.
  • The system never has a "flag day" upgrade requirement.

What you may do

  • Add a new optional field to a schema. Old payloads without it still validate.
  • Add a new op to static accepts. Old callers don't know about it; new callers use it.
  • Add a new event name to static emits. Old subscribers don't see it; new subscribers do.
  • Tighten internal validation AS LONG AS the wire shape doesn't change.
  • Add a new error code to your actor's errorSpecs.

What you may NOT do

  • Remove a field from a schema. Old callers may still send it; old consumers may still read it. Removing breaks both directions.
  • Rename a field. Equivalent to "remove old + add new".
  • Change a field's type (e.g., string → number).
  • Make an optional field required. Old payloads will fail validation.
  • Remove an op from static accepts. Older callers may still invoke it.
  • Change an op's semantics so that an old payload now produces a different result.

If the field truly must go away, deprecate it: keep it in the schema, mark it deprecated: true in description, ignore it in the implementation, and wait for old callers to migrate. Plan a removal release with explicit version-skew testing.

Idempotency requirement

Every write op MUST accept an optional idempotencyKey:

Source: CLAUDE.md Rule 9. "Every write op accepts optional idempotencyKey."

This is a forward-compatibility hedge: if the actor adds dedup logic later, callers' retries become safe automatically. Today, most actors simply pass through; the field is there for the future.

Pagination requirement

Every list op MUST return cursor-based pagination:

"Every list op returns cursor-based pagination."

Today many list ops return { ok: true, count, items } without a cursor. The migration target is { ok: true, count, items, cursor } where cursor is opaque. The cursor field is additive — adding it does not break clients that ignore it.

Wire compatibility

The wire grammar ({root}.{mount}.$facet) is stable. Subject mapping in NatsTransport.semanticToNats does not change between releases without a major version bump. Adding a new facet (e.g., $stream.<name>) is additive at the transport layer.

Envelope compatibility

The MxEnvelope shape is stable. New optional fields may be added (traceId/spanId/parentSpanId were added without breaking older code that didn't carry them). Required fields are fixed: op and payload.

Reserved subjects

The reserved facet list (Reserved subjects) is the protocol's ABI. Adding a new reserved facet is allowed in minor versions if it is genuinely additive (the new facet has its own suffix and doesn't collide with existing ops). Removing a reserved facet requires a major version.

Discovery metadata compatibility

Per WORKSTREAMS/core-and-packaging/MATRIX-DISCOVERY-METADATA-SPEC.md:

  • appName, spacePath, spaceId, authorityRoot field names are stable.
  • New optional metadata fields (e.g., a new tags shape) are additive.
  • The v1 → vNext manifest migration (planned) carries existing fields through; no removal in v1.

Practical migration recipe

When you need to change an existing field:

  1. Add a new field with the new shape (logicalMountV2 next to logicalMount).
  2. Have new code prefer the new field; old code keeps using the old one.
  3. Have the actor populate both for a few releases.
  4. Mark the old field deprecated.
  5. Eventually (next major), remove the old field — coordinated with client release notes.

This is the path the routeKey → spacePath migration is on (see Authority roots).

What is NOT additive-only

Some changes are intentionally not in scope of this rule:

  • Capability tokens. These are short-lived; new caveats can be introduced and rejected by older actors that don't recognize them (the actor must reject unknown caveats, per the capability model).
  • Trace context. New fields on the envelope (traceId, etc.) are optional — old code ignores them. Within trace processing, the semantics may evolve.

See also