AI-assistance disclosure: I used AI for source discovery, structure, and editing. I manually reviewed the technical claims against the public code and primary platform documentation, and I stand behind the analysis.
A disappearing row proves only that one interface stopped rendering it. Engineers who promise deletion have to define what happens across clients, relays, databases, caches, logs, queues, replicas, backups, and recovery paths.
Deletion is an end-to-end property
In a distributed system, data rarely has one address. A user action can produce an application record, search index entry, cache value, analytics event, retry payload, queue message, log line, database replica, backup block, notification preview, and one or more client-side copies. Hiding the application record does not resolve the rest.
A credible deletion promise therefore needs a scope, an authority, a deadline, and a failure model. Scope identifies the copies the service controls. Authority identifies who may trigger destruction. The deadline says when controlled copies become inaccessible and when they are physically reclaimed. The failure model explains what happens when a component is offline, a retry arrives late, or recovery restores older state.
Start with a state inventory, not a delete endpoint
Before designing an API, list every place the data can exist and why it exists there. Classify each location as authoritative storage, derived storage, transit, operational telemetry, backup, or participant-controlled state. Give every class an owner and a retention rule. If a team cannot enumerate a copy, it cannot make a defensible claim about deleting that copy.
| State class | Typical examples | Deletion question |
|---|---|---|
| Authoritative | Primary database, object storage | What event makes the record unreachable? |
| Derived | Indexes, caches, thumbnails | How are stale derivatives invalidated? |
| Transit | Queues, retries, relay buffers | Can an old payload recreate deleted state? |
| Operational | Logs, traces, analytics | Was sensitive content excluded before collection? |
| Recovery | Replicas, snapshots, backups | How is destruction preserved after restore? |
| Participant | Downloads, screenshots, exports | Which copies are outside service control? |
Inventory every controlled copy before defining a deletion promise.
Model destruction as an irreversible state transition
A boolean such as deleted=true is usually too weak. Systems often need a lifecycle that distinguishes an active object from one that is closed to new writes, destroyed for normal access, and eventually reclaimed from recovery media. The transition into the destroyed state should be monotonic: delayed requests, retries, and replayed events must not reopen it.
ACTIVE -> SEALED -> DESTROYED -> RECLAIMED
| |
+--expire--+
Invariant: no transition leaves DESTROYED or RECLAIMED.
Store the destruction marker wherever the system stores the object's identity, and make every write path check it. If a backup restore can resurrect an older active record, the recovery procedure must replay later destruction markers before the object becomes reachable.
Retries and restores may move forward, never out of destruction.
Separate the control plane from the data plane
The control plane answers whether an object exists, who may use it, when it expires, and whether destruction has occurred. The data plane carries the content. Keeping these responsibilities separate makes it possible to retain the minimum state needed to enforce an irreversible tombstone without retaining the content that the tombstone is meant to retire.
This separation also sharpens review. A reviewer can ask whether the control plane authorizes destruction correctly, whether the data plane writes content anywhere durable, and whether either plane leaks content into logs or metrics. Encryption helps with content confidentiality, but it does not answer lifecycle, metadata, or endpoint questions by itself.
Design for the failures that make data return
Most deletion bugs are resurrection bugs. A mobile client reconnects with an old offline mutation. A queue retries a create event after a tombstone. A cache repopulates from a lagging replica. A restore process brings back a record but not the later delete. A search index remains queryable after the primary row is gone. These are ordinary distributed-systems behaviors, so deletion tests must exercise them deliberately.
- Make write operations conditional on the current lifecycle state.
- Give destructive transitions stable, idempotent identifiers.
- Expire queued and offline mutations before they can outlive their purpose.
- Propagate invalidation to indexes and caches through observable workflows.
- Keep content out of logs, traces, error reports, and analytics by construction.
- Document backup retention separately from interactive deletion latency.
Verification needs negative evidence
A successful API response proves that one component accepted a request. It does not prove that the system can no longer serve, search, replay, restore, or infer the data. Verification should test absence across every controlled state class and should repeat those checks after component restarts, delayed delivery, cache refresh, replica catchup, and recovery exercises.
- Create uniquely identifiable test content without using real sensitive data.
- Confirm each intended state class receives—or deliberately never receives—it.
- Trigger expiry and explicit destruction through every authorized path.
- Attempt reads, writes, searches, reconnects, retries, exports, and sync operations.
- Restart components and replay delayed messages that predate destruction.
- Restore a pre-destruction snapshot, then apply the documented recovery procedure.
- Check logs, traces, metrics, crash reports, indexes, and caches for the marker.
- Record which participant-controlled copies remain outside the service boundary.
Verification seeks negative evidence across restart and recovery paths.
User language should match the system boundary
"Gone forever" is almost never an engineering statement. A service can promise that it no longer serves content, that controlled content stores were purged, or that backups age out within a documented window. It cannot promise that a recipient forgot, that a screenshot vanished, that a compromised endpoint was cleaned, or that network metadata was never observed unless the architecture actually provides those properties.
Product copy should name the actor and the scope: "the relay no longer retains the room" is testable; "this conversation leaves no trace" is not. Honest language is not a conversion penalty. It is part of the interface contract, especially when users are choosing a tool because they want less durable data.
elm.chat as an inspectable, imperfect case study
elm.chat applies a narrow version of this model. A Durable Object stores room policy, status, creator capability, and invite state. Connected browsers hold the current conversation history. The relay forwards encrypted messages and file chunks without persisting a server-side transcript. Destroying the room changes its control-plane state so later joins and writes are rejected.
The tradeoffs are explicit. Cloudflare can observe IP addresses, connection timing, sizes, and presence. Participants can retain plaintext. Peer-supplied history is not a trustworthy archive, and message authentication plus replay and duplicate protection remain unfinished. The project has not had an independent security audit and is not an anonymity, compliance, whistleblowing, or high-risk communications system.
The design review question
Do not ask only, "Where is the delete button?" Ask, "Which controlled copies can still influence behavior after destruction, and what evidence proves they cannot bring the object back?" That question turns deletion from a user-interface gesture into a system property engineers can model, test, monitor, and explain.
The complete source, architecture, threat model, and deployment path are public under AGPL-3.0 for anyone who wants to challenge the example or adapt the design to a different infrastructure.
Originally published at elm.chat. Cross-posted here with the original URL set as canonical.
Top comments (0)