ADR-0001: Content-Addressed Document Storage with PostgreSQL as Source of Truth¶
- Status: accepted
- Date: 2026-06-04
- Deciders: Nicolas Burri
- Refined by: ADR-0017 (multi-part content), ADR-0029 (content model — draft-freeze, content RLS, append-only audit), ADR-0045 (image blobs as parts)
Context¶
LQMS must store and version QMS documents and records. The decisive forces:
- Audit reproducibility (ISO 13485): the exact content and approval history of any released document version must be reconstructible, years later.
- Strict mandator separation: the storage layout must not be a place where data separation can leak (top quality goal, see arc42 §1.2).
- Backup: zip-based export/restore of the whole system or a single project, restorable on a different server.
- Storage evolution: files on the local file system initially, behind an interface that allows moving to an S3-compatible object store (requirement from project_overview.md).
- Search: RAG indexing needs clean, permission-aware access to content.
- Content model: documents are both authored in-tool (structured content for SOPs/WIs) and uploaded files (PDF, office documents, test evidence as records).
Options Considered¶
Option A: PostgreSQL-only¶
All content, including binaries, stored in the database; explicit version tables.
- Pros: single source of truth; full transactional consistency; backup is a DB dump.
- Cons: binary documents bloat the database; the required S3-compatible interface would have to be retrofitted later against a harder migration.
Option B: Git-backed content¶
One git repository per project holding document content; metadata in PostgreSQL.
- Pros: versioning, immutable history, and text diffs for free; repo-per-project aids separation; backup via git bundle.
- Cons: two sources of truth — life-cycle state in the DB and content in git cannot share a transaction; git's model (branches, merges) maps poorly onto document life cycles; server-side concurrent repository manipulation is error-prone; binary files diff poorly; no natural S3 path.
Option C: PostgreSQL as source of truth + content-addressed blob store¶
PostgreSQL owns all state: document metadata, version records, life-cycle states, approvals, audit trail. Version content is stored as immutable, write-once blobs keyed by their SHA-256 hash behind a storage interface (file system now, S3-compatible store later).
- Pros: matches the stated storage-evolution requirement; hash-keyed immutable blobs give cryptographic integrity verification of released content; consistency is simple by construction (blob writes are idempotent and precede the DB transaction — a crash can only leave an orphan blob, never an inconsistent document); blob storage is partitionable per mandator/project, making separation visible at the storage level; per-project backup = DB export + referenced blobs.
- Cons: version semantics must be built explicitly in the schema (needed for configurable life cycles anyway); orphan blobs require a garbage-collection job; text diffing must be implemented separately if wanted.
Decision¶
Option C. PostgreSQL is the single source of truth for all document state; version content is stored as immutable SHA-256-keyed blobs behind a storage abstraction with a file-system implementation first and an S3-compatible implementation later.
Git remains available as a view (e.g. a future "export project history as git repo" feature for engineer-friendly diffing) but is not the system of record.
Consequences¶
- A storage interface (
put(content) → hash,get(hash),exists(hash), …) is part of the core architecture; implementations: local file system (now), S3 (later). - Write protocol: persist the blob first, then commit the DB transaction referencing its hash. A periodic garbage collector removes unreferenced blobs.
- Authored content (in-tool editing) lives in the DB while in a mutable draft state; on every life-cycle transition that freezes content (e.g. submit for review, release), it is serialized to a canonical form, hashed, and stored as a blob like any upload. Released content is therefore always an immutable blob, regardless of origin.
- Blob layout must encode the mandator/project partition (e.g.
<mandator>/<project>/<hash-prefix>/<hash>) so per-project backup and separation audits operate on the file tree directly. Identical content in two projects is stored twice — deduplication is explicitly sacrificed for separation. - Integrity checks (re-hash and compare) can run as part of backup creation and audits.
- The version/lifecycle schema design follows in a separate ADR once the domain model discussion is complete.