An object key in the documents bucket is blobs/<sha256> — the address is the content hash, so filing the same bytes
twice costs one object instead of two. That is the right instinct for storage and the wrong one for a law firm, because
the identical PDF is exactly the case you should expect: a blank government form, a recorded deed, a protective order,
an exhibit filed on two related matters. Dedup on the hash alone means one object backs two clients' documents, and the
matters are now coupled through a bucket key that nothing in either file mentions.
That coupling only shows its teeth on deletion. A governed expunge — a privilege clawback, a sealing order, a lawful
deletion request — rewrites the matter's repository history and deletes the document's bytes. If the object were shared,
deleting it on the sealed matter would also empty an unrelated client's file, on the authority of an order that never
named them. If that second matter were under a preservation duty, the firm would have destroyed evidence in a case that
had nothing to do with the one it was acting on, and it would have no record of doing so.
So dedup is scoped to the matter: store::documents::ingest_bytes reuses an object only when an asset row on the same
project already points at it. Two matters holding the same exhibit get two objects. The storage cost is a rounding
error against a single spoliation finding.
Scoping ingest fixes what gets written from here on; it does not unshare what was already written. So expunge carries
the matching guard: before deleting a key it asks whether an asset row on another matter still references it, and if one
does it keeps the object and emits a warning naming the key. That is deliberately loud rather than silent — an order
that requires the bytes actually destroyed now needs a person to reach the other matter and deal with it there, which is
a decision for a lawyer and not for a delete loop. An asset row carrying no project at all does not block the deletion:
it is unattached rather than another matter's, and letting a stray row veto a privilege clawback would defeat the one
thing this primitive exists to do.
Together the two rules give you the property that matters when you are the one answering for the file: no matter's
documents are held hostage to another matter's, and no order deletes bytes it did not name.
When multiple deployments share a project, pass their explicit bucket, SQL, VPC, subnet, cluster, namespace, gateway-IP,
and service-account variables. The single-project <project>-suffix defaults remain convenient for an independent OSS
install, but the three-row Navigator matrix never relies on them.