Protocol specification: delete and failure
Delete and purge semantics and failure semantics.
Published from docs/protocol-specification/delete-and-failure.md (GitHub)
Part of the protocol specification.
Delete And Purge Semantics
Removal is terminal and structural. Link, unlink, share, revoke, rekey, move, and document purge carry signed access events; container delete is an authenticated control-plane operation without a signed artifact.
POST /documents/:documentId/purge accepts a signed document.purge event. The
signature binds the current document manifest, the sole linked container, and
every manifest hash in the exact current root-to-leaf container path that
authorized the purge. The API independently verifies the event, exact heads,
root completeness, path contiguity, organization scope, and signer write access,
stores the terminal event, and then removes the document’s mutable row, content
keys, encrypted updates, attachments, and live-head pointer. It retains the
signed document manifest chain and the exact signed authorization path plus its
verification dependencies so a device authorized through that purge-time path
can retrieve the proof from
GET /documents/:documentId/purge after the live document is gone. A user whose
only access came through a path unlinked before the purge cannot retrieve it.
The proof carries redacted principal-policy snapshots for every group head its container evidence references. Each snapshot retains the signed public state chain and committed membership/grant projections, but no encrypted payload, epoch-key row, or member envelope. Clients verify those signatures and commitments before using historical group membership. Group deletion can therefore erase recoverable server-side key material without making an earlier terminal purge unverifiable.
Purge-path access permits a terminal snapshot. A caller’s recorded observation of a signed document head also permits full history; an explicit earlier checkpoint requires that caller’s observation of that exact head. History includes cited public container/policy evidence, not keys. The SDK authenticates before reading local pins (verified manifest checkpoints) and verifies one response without refetching. Deletion requires an exact pin or signed transitions from a pin or genesis. Missing history with no local checkpoint defers deletion without an incident. A conflicting checkpoint records an incident; snapshots cannot advance it. A newer path pin, ancestors included, withholds deletion without incidents or retry loops: ancestry cannot order the purge signature. The SDK commits the terminal purge checkpoint in the same local SQLite transaction that removes the matching document row and its side state. A stale local generation, identity replacement, failed cleanup, or interruption rolls back both the checkpoint and deletion so the verified proof can be retried safely. After a lost POST response, a coded not-found retry uses the retained proof.
The API also requires that the document is linked to exactly one container (a
document with several links must first be unlinked down to one) and that the
target is not a container metadata document, which is torn down only with its
container. An unknown document id returns not found. Blobs referenced only by
the purged document are soft-deleted under the
attachment retention lifecycle: dereferencedAt is
stamped (an existing stamp keeps its original time) while bytes, objects, and
key material wait for the garbage-collection sweep, and a bind during the grace
period revives the blob. A blob still referenced by another active binding is
untouched; detached bindings do not keep a blob live. The response returns
purgedAt.
DELETE /containers/:containerId is an admin-only structural delete for empty,
non-system, non-root leaf containers. A root or system-slot container cannot be
deleted. Child containers or any linked user document make the delete a
conflict; the container’s own metadata document, which every container links to
itself, is excluded from that guard. An unknown container id returns not found.
A successful delete runs in one transaction: it writes a per-recipient
deleted sync tombstone (the peer discovery signal), removes the container
row, then tears down its metadata document: the containerMetadataDocuments
binding and, when the composite create path materialized document rows, those
rows and any blobs they orphan (dereferenced after required row locks using the
database wall clock, so lock waits cannot consume the garbage-collection grace
period). The metadata document gets no purge tombstone because it is withheld
from client discovery; the container tombstone is the sole peer signal. Any
guard failure rolls the whole transaction back.
Trash is itself a system-slot container and so cannot be deleted or purged.
Sending an item to Trash is an ordinary signed relocation — never a delete — and
stays reversible. A document is relocated by replacing its link set (link Trash,
unlink its source containers); a container is relocated by re-parenting it under
Trash with container.move. The move workflow guards only the moved container’s
own system slot, not the destination’s, so a normal folder and its whole subtree
may be moved into Trash, and moved back out again to restore it. Terminal
removal is the separate, structural step:
POST /documents/:documentId/purge for a document and
DELETE /containers/:containerId for a folder — a non-empty folder is torn
down by a client-orchestrated cascade of those two primitives applied
leaf-first.
Failure Semantics
The protocol is fail-closed:
- malformed route JSON returns validation errors
- stale manifest heads, stale key targets, and stale recipient keys return conflicts
- missing signed policy material prevents managed-principal key derivation
- signer/session mismatches return forbidden
- unavailable policy, key, blob, or document material is an availability failure, not permission to bypass verification
Projection tables are indexes and caches for list and lookup performance, not authority for encryption targets or access proofs.