A document's bytes and authorization are separate
Inside the documents bucket, the storage key is content-addressed: store::documents::ingest_bytes writes bytes at
blobs/<sha256>. The Surreal asset row is the Project-scoped record: it carries the Project association, provenance,
visibility, and the reference to that object. The portal checks that row before streaming a document, so clients never
receive bucket credentials or direct object URLs.
The ingest deduplication decision is scoped to the Project, even though the key shape is not: an existing matching asset on the same Project avoids a second write, while a matching asset on another Project does not authorize reuse of its row. Because two rows can name the same content-addressed key, a governed expunge checks whether another asset row still references the object before deleting its bytes. That reference check keeps a deletion in one Project from removing bytes still needed by another.
This is why Project growth does not require a documents bucket per Project. The bucket is a private deployment lane; the Surreal record supplies the Project boundary, and the application supplies the authorization boundary. The applications lane uses a different shape: each published portal is under its Project-code prefix and the publisher's IAM condition enforces that prefix.