Skip to content

Files and blobs

Partial

Files keep the opaque bytes that go with a fleet (a firmware image, a config dump, a runbook, a packet capture) searchable and deduplicated: a file handle over a content-addressed blob store, behind the same Storage Gateway as everything else.

  • file is indexable metadata: a filename, content-type, size, sha256 (no tags column yet, deferred to #191), the searchable handle an operator references; it owns no bytes, pointing at a blob by hash. The filename is a human label with an extension (codec-firmware-2.1.4.txt), deliberately not a triad name: it takes no name rule, carries no uniqueness, and the handle is addressed by its uuid, which is why file is declared “human identifier, not a name” in the identity shapes.
  • the blob store holds the bytes, content-addressed by sha256: the hash is the key, so identical bytes are one blob.

Splitting them means search and inventory never touch bytes, and one blob can back many handles.

Access: a permission and a sensitive tier, no placement

Section titled “Access: a permission and a sensitive tier, no placement”

A file carries no fleet placement: unlike a secret, which is for one component or system and sits on the exclusive arc, a file relates to many things (a firmware image documents many device types) or to nothing, so its locality is its future attachments (a many-to-many relation), not an owner column. A file is tenant-wide, gated by two existing layers:

  • the file:<action> permission on every route (file:create, file:read, file:delete; download rides file:read);
  • a per-file sensitive flag reusing the secret sensitivity tier (ADR-0025): a flagged file is lifted to the :admin tier (file:read:admin), hidden from a lister and a non-disclosing 404 to a reader without it (a competitive quote is a sensitive global file, a firmware image an ordinary shared one).

Unlike a secret, the flag defaults false, and file is not in the sensitive-resource set, so the viewer floor (*:read) reads ordinary files; only the flag fences the confidential ones.

A blob is keyed by the hash of its bytes, not a UUID, which buys:

  • dedup: identical bytes collapse to one blob (two operators uploading the same firmware);
  • integrity: the hash verifies the bytes on read, tamper-evident by construction;
  • immutability: bytes cannot change without changing the key;
  • backtest-stability: an event referencing a hash still resolves under a backtest.

The blob key is sha256, the bare content hash, no tenant_id: isolation is per-database (a database per tenant), so dedup is global within a database and one tenant can never detect another’s content by hash collision, because the blobs never share a store; the efficiency cost is the right price for physical isolation.

The bytes live behind the Storage Gateway, so the backend swaps with no model change (the same seam as the columnar and object tiers):

  • default: pgblobs (a dedicated Postgres blob table), the single-binary, no-external-dependency story;

The handle and the content-addressed bytes; the physical layout (the gateway, GC) is above and on storage.

file (Partial) is the searchable metadata handle, pointing at a blob by hash: tenant-wide, sensitive per the access section above.

file (content): generated from the migrated schema by make gen
Column Type Constraints Notes
id uuid PK, default uuidv7()
name text not null
content_type text not null
size bigint not null
sha256 text FK → blob.sha256, not null The content address of the bytes this handle names
sensitive boolean not null, default false Lifts the row to the :admin tier
created_at timestamp with time zone not null, default now()
updated_at timestamp with time zone not null, default now()

blob (Partial) is the content-addressed bytes: dedup on the hash, default pgblobs (inline bytea), S3 / disk behind the same blob.Store seam. Content type lives on the file, because content-addressing is about the bytes. A file delete frees its unreferenced blob synchronously (dedup-aware); async mark-sweep GC is deferred.

blob (content): generated from the migrated schema by make gen
Column Type Constraints Notes
sha256 text PK The content address; dedup key across every referencing file
bytes bytea not null
size bigint not null
created_at timestamp with time zone not null, default now()