---
name: w3ds-destructive-writes
description: "Use before any write that touches a record which already exists — updateMetaEnvelope, editing a profile, attaching a signature to an existing event, re-sharing, changing an audience, uploading a file to a vault, or any 'just add this one field' change. Also use when deciding how to confirm a vault write succeeded. eVault has no history and no undo, so a careless update is permanent."
license: Apache 2.0
---

# Updates destroy. Plan for that.

There is no history surface in eVault. An inspection of its GraphQL API found **eight queries and twelve mutations and zero history types** — no versions, no audit of the record itself, no restore. Whatever an update overwrites is gone.

Everything below follows from that.

## An update replaces the whole payload

`updateMetaEnvelope` is not a patch. It rewrites the record from what you hand it. A write built from a partial read silently erases every field it did not carry.

The procedure, not the intention:

1. **Read the full body first.**
2. **Snapshot it to a file** before writing. Not to a variable — to a file you can still read after the process exits.
3. Merge your change into the snapshot.
4. Write.

Step 2 is the one people skip, and it is the only one that helps after the mistake.

## The audience is part of what gets rebuilt

The same call, on the two ACL models, has opposite consequences — this is the sharpest reason the models must never be reasoned about as one thing:

- **Legacy `acl`**: it is an input to the mutation. An update that omits it does not carry the previous audience forward. Under the old default it falls back to `["*"]` — the record becomes public as a side effect of an unrelated edit.
- **Newer `_acl` block**: the specification says an update that does not carry `_acl` leaves the stored policy alone.

The concrete bite, reported and fixed 2026-09-15: an `attestAuditEvent` routine rewrites a revocation event to attach a signature. The naive version carried the new signature and nothing else — and would have rewritten the audience of a published record while appearing to add one field.

Before any update, state which model the record uses and what happens to its audience. If you cannot tell, that is the finding.

## Ask the owner when the change loses information

Two cases require an explicit confirmation in the user's own words before writing — old value → new value, what stays, what becomes inconsistent:

- the record was **authored by another platform**, or
- the change **loses information**.

Both are situations where the write is defensible and still wrong, because the person who has to live with the result did not choose it. "It was technically correct" is not a defence against an overwrite nobody asked for.

## Uploading a file is two operations

Putting a file on a vault is **two writes, not one**:

1. `uploadFile` stores the blob under ontology `w3ds-file-v1`.
2. The catalogue that the owner's apps list and search is a **different ontology** — `File` — and must be created separately.

Do only the first and the file is genuinely on the vault and genuinely unfindable: present in storage, absent from every list.

A real `File` record carries **both naming conventions at once** (`url` *and* `publicUrl`, `name` *and* `filename`, `mimeType` *and* `contentType`). Copy that shape whole rather than picking the one that looks canonical — different readers read different halves.

And remember where it lands: `X-ENAME` selects the tenant on writes, so an upload meant for someone else's vault must be addressed **as them**, or it lands on yours.

## Record the outcome of the remote write, not the intent

The commonest way a platform lies to the ecosystem: the local database decides the new state, the vault write goes second over the network, and **nothing ever comes back to the pairs that diverged**. The authoritative object then advertises the old state to every other application, while the platform's own screens show the truth. Nobody sees a bug; the platform looks fine from inside.

Measured on one graph, 2026-09-15: `Account` — 22 envelopes, every `updatedAt` on 2026-03-28, untouched for five and a half months — while `Ledger` ran to 2026-09-13 with 208 envelopes. **Balances moved; the accounts that state them did not.**

So: the local row records what the remote write *returned*, not what the platform decided. A failed or skipped vault write must be visible as a failed vault write, and must be retried by something.

### Writing correctly is not the same as being seen

Three times in one evening a technically valid envelope went unnoticed, each for a different reason:

- a **chat message** — the recipient's index is fed by the Awareness pipeline, not by reading vaults;
- a **reminder** — whoever displays it is a separate application;
- a **task** — it needed the right owner, the full field set **and** a reference envelope in the reader's tenant.

Verify the reader, not just the write. "The envelope exists" answers a question nobody asked.

## Verify by a bounded recent sweep, not on the write path

Read-back verification is right; doing it after every write is not. Reading on the write path doubles traffic to somebody else's storage for a reassurance that is almost always identical — and deployments are rate-limited (ours at 100 requests/minute).

The better shape: on deploy, and periodically, read the **most recent N canonical records** off the vault and compare them against local state. A window over the newest records finds a lying write within a day rather than never.

It also catches drift **you did not cause**, which no column of your own ever could — the whole point of W3DS being that other applications write to the same objects.

### Never let a read-back search on the value you are testing

A verification that looks for the value it just wrote proves only that the write happened, not that it went where it belongs.

Measured 2026-09-16, and it is my own mistake. A console listing printed `chat=4ec3bab9` truncated to eight characters. I completed it into a full UUID from nothing and sent a reply. The message was created on the recipient's vault and belongs to no chat — invisible while existing. The verification **passed**, because the read-back searched `chatId = <the same invented value>`, found the message it had just written, and reported success.

Two rules out of that one incident:

- Take every identifier from the **record**, never from a truncated column or a rendered view.
- A read-back must confirm the record is reachable **the way a real reader reaches it** — in the listing, in the chat, under the reader's own eName — and should assert something it did not write (for example, that other participants' records are present in the same collection).

## A guard belongs where the thing it constrains happens

A rule lives where the action occurs, not where the domain object is defined.

Measured 2026-09-15: an `assertAudience` guard sat in the envelope module and therefore covered **two of the four** places that codebase rewrote a published record. Moved next to the write client — because the rule is about *writes*, not about *envelopes* — it covered all four, including a notification audience rebuilt from two constants nobody had asserted must be there.

Placement silently decided coverage, and nothing was broken by the gap. That is what makes this failure quiet by construction. When a guard exists, ask what it is a rule **about**, and put it there.

## Checklist before an update lands

- [ ] The full existing body was read and **snapshotted to a file**.
- [ ] Which ACL model the record uses is known, and the audience's fate under this update is stated explicitly.
- [ ] If the record was authored elsewhere, or the change loses information, the owner confirmed old → new in their own words.
- [ ] A file write created **both** the blob and the `File` catalogue record, with both naming conventions.
- [ ] The local row stores what the remote write returned; a failed vault write is visible and retried.
- [ ] Verification reaches the record the way a reader does, and asserts something it did not itself write.
- [ ] No identifier in the write came from a truncated or rendered source.
