Scope, backfill, eviction and deletion
1. The scope mechanism
Section titled “1. The scope mechanism”Not everything a node stores may leave it. SSP/1 bounds replication in two tiers: a hard floor that policy cannot override, and a policy layer above it.
1.1. The floor
Section titled “1.1. The floor”A conforming mesh MUST define a floor: a partition of its tables into three categories, fixed by the implementation rather than by configuration.
| Category | Meaning | Policy may reach it |
|---|---|---|
| Syncable | May replicate, subject to policy | Yes |
| Never syncable | Structurally excluded | No — unrepresentable in policy |
| Node-local derived | Reconstructible locally; replicating it is waste | No |
The floor is a mechanism, not a table of names. SSP/1 does not enumerate which schemas fall where, because that is a property of the application built on it rather than of the protocol. What SSP/1 requires is that the partition exists, that it is identical on every node of the mesh, and that the never-syncable category is structurally unrepresentable in policy rather than merely denied by default.
The distinction matters. A deny-by-default rule can be overridden by a misconfiguration or a widening operation. Structural exclusion cannot, and the categories that belong there are the ones where a single mistake is unrecoverable: credential and key material, transport state, and anything whose replication would leak the mesh’s own trust configuration.
Enforcement is dual-sided. A producer applies the floor in its scan predicate and treats a local violation as an error, because a producer emitting out-of-floor data has a bug that silence would hide. A receiver drops and logs, because raising there lets a hostile peer wedge the apply loop.
1.2. Policy above the floor
Section titled “1.2. Policy above the floor”Each node holds a local policy store that is never replicated, keyed
(peer_site_id, table_schema, table_name, layer) with effect allow or deny.
* is a valid wildcard for peer and for table. It is never valid for schema:
a schema-level wildcard is how an accidental widening becomes a total one.
Layers are ordered system < role < user. Resolution for a given
(peer, schema, table):
- Outside the floor, deny unconditionally.
- The highest layer containing any matching row wins.
- Within that layer, the most specific undominated row wins: an exact peer beats
*, and an exact table beats*. - Two incomparable maximal rows: deny and log. Never guess open.
- No matching row: deny. Fail closed.
Policy is a sender-side decision and never appears on the wire. A receiver therefore cannot widen its own feed by lying about its policy, because it is not the one consulting it. This is why scope is enforced where the data lives rather than where it is wanted.
Narrowing is always permitted. Widening is floor-checked and triggers backfill.
1.3. Role assignment records
Section titled “1.3. Role assignment records”The role layer needs one thing the other layers do not: the assignment itself
must travel between nodes, or two nodes resolve policy against different role
views with no signal that they disagree.
A role assignment is a chained COSE_Sign1 record — the same chained-record shape
the object plane uses for authorization records and sequencing receipts, one
mechanism instantiated per authority:
RoleAssignment = COSE_Sign1( protected = { alg : EdDSA, content type : "sovm/ssp-role-v1", kid : issuer site_id (16 bytes) }, payload = deterministic CBOR { subject_site_id : bstr 16, role : tstr, generation : unsigned, previous_record_hash : bstr 32 / null }, external_aad = mesh_id (16 raw bytes))Rules:
- The issuer MUST be authorized by the mesh’s trust policy to assign roles. A record from an unauthorized issuer is dropped and surfaced.
- Records chain per subject:
generationis monotonic for thatsubject_site_id,previous_record_hashis the BLAKE3 hash of the exact bytes of the record it supersedes, and the genesis record carries generation0with a null predecessor. - The current role is the head of the subject’s chain. A verifier holding two
records that claim the same predecessor MUST surface the conflict and MUST
resolve the subject’s
rolelayer as absent until it is resolved — which is deny-safe, since resolution fails closed on a missing layer. - Records travel in the sync exchange’s
recordsmember or through the mailbox.
The chain is why two nodes cannot silently disagree: either they hold the same head, or one holds a strict prefix of the other and converges on delivery, or the chain has forked — and a fork is detectable evidence of a misbehaving issuer, not a quiet divergence.
2. Widening backfill
Section titled “2. Widening backfill”When policy widens, history that was previously withheld must flow, or the peer’s view stays permanently incomplete in a way no live tail repairs.
The sender enqueues a durable job per (peer, schema, table) that re-sends the
latest event per row, ordered by row_key, resuming from a durable
resume_after_row_key cursor.
Requirements:
- A backfilled event is the original
COSE_Sign1message, byte for byte. It is a re-delivery, not a new event: the derivedevent_idis therefore unchanged, and a receiver that already has it converges to a no-op via idempotent apply. Re-encoding or re-signing is forbidden — it would mint a new identity for an old mutation and attribute it to a new moment. - The sender MUST re-check policy at emit time and drop rows a standing narrow has since excluded. Policy can change while a backfill is in flight.
- Live tail SHOULD be sent before backfill within a cycle, so current activity is not starved by history.
- Backfill progress MUST commit only after a successful push.
- Implementations SHOULD bound rows per cycle. The bound is a resource decision, not a protocol invariant.
3. Eviction is not deletion
Section titled “3. Eviction is not deletion”A storage-constrained node MAY evict cold history below a per-table HLC horizon. Eviction is strictly local and MUST NOT be confused with deletion.
Requirements:
- Eviction MUST NOT emit any event. It is a local storage operation, and emitting one would replicate a local capacity decision as a semantic one.
- The horizon MUST be monotonic non-decreasing.
- Events below the horizon count as applied for watermark purposes but write nothing. A peer must not be asked to re-send data the receiver deliberately discarded.
- Producers MUST suppress deletes inferred from the absence of evicted rows. This is the trap eviction sets: a naive change-detector sees a missing row and concludes it was deleted, then replicates that conclusion to peers that still have it.
- Only categories the implementation declares evictable may be evicted, and the declaration MUST be documented.
Rehydration zeroes the horizon and relies on backfill to re-deliver.
Ambiguous eviction markers MUST fail open — keep the row. This is the one place SSP/1 prefers availability over strictness, and the reason is that the cost of being wrong is asymmetric: failing open wastes local storage, failing closed destroys data.
4. Deletion
Section titled “4. Deletion”Two modes, one of which is not available.
4.1. Soft delete
Section titled “4.1. Soft delete”An operation: "delete" event with a null payload. It replicates and merges like any
other event, participates in
causal-length existence, advances the
tombstone clock, and is resurrectable by a
later higher-HLC upsert.
A soft delete hides a row and clears its column values. It does not erase history: the retained log is what makes resurrection rebuild possible.
4.2. Purge
Section titled “4.2. Purge”Reserved and not available. The "purge" token is registered so that no future
extension reuses it, but no purge message format is specified.
An implementation MUST fail a purge request closed — an explicit error, never a silent downgrade to soft delete. A caller that asked for permanent erasure and received a tombstone has been given a false assurance, and that is worse than a rejection.
Where permanent erasure is required, the mechanism is destruction of the keys protecting the data at a layer above this one. See limitations.