ADR-0006 — Immutable releases enforced by the database, with a separate revocation state
Status: accepted, 2026-11-09.
Decision
- A release row is written once. The trigger
releases_immutablerejects anyUPDATEthat changes the artifact, version, manifest, object key, checksum, sizes, file count, source commit, README, publisher, publication time or scan status, and rejects everyDELETE. - Only three things can change afterwards:
state(published,quarantined,revoked), the revocation fields, andrelease_notes. release_files,release_dependenciesandaudit_eventsare append-only by theappend_onlytrigger.(artifact_id, version)is unique. Publishing an existing version returns 409version_exists.- The blob is content-addressed and read-only on disk (ADR-0003).
- A compromised version is revoked: downloads return 410 with the public reason, the record and its audit trail stay, and
latestmoves to the best remaining version (highest stable, otherwise highest prerelease). - The API maps trigger violations to 409
release_immutable. - The scope
releases:updatepermits editing release notes and nothing else.
Why triggers
Application-level checks protect against honest mistakes in one code path. Triggers also cover future code, the admin console, and ad hoc SQL by an operator. The tests update and delete rows directly with SQL and assert that PostgreSQL refuses.
Consequences
- There is no "unpublish". Removing content requires revocation (version) or takedown (artifact); erasing bytes for legal reasons is a manual operator procedure that must itself be recorded.
- Release notes are mutable metadata; edits are audited with before/after lengths, not content.
- Approval of a quarantined release changes
stateonly. The advisory scan status recorded at publication never changes.