Skip to content
Registry StackDocsDevelopment (unreleased)

Retain, erase, and audit

For the operator

View as Markdown

You run an active registry under an institutional retention policy and hold its migration credential. This page shows you how to erase retained record history and Proposal detail, restore snapshot coverage afterwards, drop expired Evidence and idempotency material, and keep the audit journal.

Every command here reads the runtime file and connects to PostgreSQL directly with the migration credential, so run it from the operator host. The erasures and the rebaseline take the registry’s migration lock: run them while no activation is in progress. A database whose certificate a private authority issued is trusted through database.trustedRootCertificateRef in the same runtime file. Every path you pass must be absolute. Reports carry counts, never record values.

The examples use the record types and Actions of the facility template.

CommandRemoves for goodLeaves in place
history eraseThe retained revisions of one record up to a revision number, the correction context and outbox payloads that referenced them, and the held responses that could replay them.The current record, every later revision, the audit journal, and each spent idempotency key.
proposal-retention eraseThe payload detail of one Proposal submission version: frozen snapshots, draft input, application reasons, Review payloads, held responses, and attachment references.The Proposal’s value-free state history, record history, and each spent idempotency key.
proposal-retention cleanup-attachmentsUnreferenced attachment bytes in external storage that are waiting for deletion.Live attachment references, Proposal detail, and deletion tombstones.
evidence-retention erase-expiredThe signed Evidence responses and verification context an Action kept, once their 24-hour window has closed.Assertions still inside their window, and the receipts of the Actions that used them.
idempotency-retention erase-expiredHeld responses past their horizon, with the raw issuer, principal value, and key each was sent under.Every spent key, as a digest, so a retry is still refused rather than run.

A saved export or a snapshot reference is not a way back: an erased revision is gone from the registry, and a reference that named it can no longer be served. A package change removes nothing from history either. A successor that drops a field leaves the old values in every earlier revision, and bregctl diff reports each one as diff.history.removed-values-retained. Only history erase removes them, and it removes whole revisions of one record, not one field.

Backups, replicas, saved exports, and copies already delivered to consumers or to an external verifier keep their own copies. Registry erasure cannot reach them.

history erase removes the retained revisions of one record up to a revision number. It is maintenance under the migration credential, not an API permission, and it does not remove the current record: apply your live-data removal policy separately.

Write the request to an owner-only file (mode 0600), so the record identifier and the reason never appear in process arguments. recordType names the record type, and eraseThroughRevision is the last revision to erase:

erasure.json
{
"recordType": "operator",
"recordId": "92f1b1e6-348c-4045-9acc-a0dc85877136",
"eraseThroughRevision": 1,
"operatorReference": "ticket-4821",
"reason": "Data subject request: remove the superseded contact details"
}

The command refuses to run without --acknowledge-irreversible:

Terminal window
chmod 600 /srv/registry/private/erasure.json
bregctl history erase \
--runtime-config /etc/breg/runtime.yaml \
--request-file /srv/registry/private/erasure.json \
--acknowledge-irreversible
Erased the requested history. 1 commit affected.
package revision sha256:1d38a738597da4a8254a53f3cb5ad6889243374134eb9c0c4dda1768fd78750b
coverage ready true
unavailable after position 0
affected commits 1
erased revisions 1
erased commit members 1
scrubbed change contexts 0
scrubbed outbox payloads 0
scrubbed cached responses 1
scrubbed Proposal targets 0
scrubbed Proposal submissions 0
removed descriptors 0

One run erases at most 10,000 revisions in one transaction. It replaces each held response that could replay an erased revision with a refusal, so a retry of that key answers 409 idempotency.conflict instead of returning the erased body. A request file that is not owner-only is refused before any connection opens. Live reads and writes continue.

Snapshot references at or after the earliest erased commit become unavailable, and erasing a record’s baseline data makes all snapshot coverage unavailable. The next package still activates when the only gap is an erasure the database recorded as committed. Any other gap refuses a successor with apply.history.coverage-incomplete until you restore coverage; apply the same package again afterwards.

Restore snapshot coverage after an erasure

Section titled “Restore snapshot coverage after an erasure”

After an erasure, a snapshot reference for the current state is refused until history rebaseline writes a new baseline. Its request file is owner-only too and carries the operator reference alone:

rebaseline.json
{
"operatorReference": "ticket-4821"
}
Terminal window
bregctl history rebaseline \
--runtime-config /etc/breg/runtime.yaml \
--request-file /srv/registry/private/rebaseline.json
Rebaselined the history. 4 records verified.

The run proves that the retained revision at the head of every live record still reproduces that record, then installs one baseline commit at the head. It restores nothing that was erased: references taken before the new baseline stay unavailable, so take a fresh one afterwards. A second run refuses with history.rebaseline.coverage.complete, because there is nothing left to rebaseline.

The outage comes from row security. The migration role has no authority to bypass it, so the run lifts forced row security on every record table and holds each table exclusively until the transaction ends. The run grows with the number of live records and retained revisions. To size the window, restore a recent backup to a scratch database, erase there, and time the rebaseline; allow at least twice that. Set operationalTimeouts.migrationLockMilliseconds long enough to wait out in-flight requests, and operationalTimeouts.migrationStatementMilliseconds long enough for one scan of the whole revision journal.

When a live record changed outside the server’s write path, the journal never saw the change, and the run refuses with history.rebaseline.live-rows.unverified without naming the record. Read the revisions of the records you suspect with GET /v1/records/{route}/{recordId}/revisions.

Proposal detail has its own retention, separate from record history. An Action whose execution.proposal.retention.mode is operator-erase lets you erase the detail of a finished Proposal; retain, the mode the facility template uses, refuses it. A Proposal is eligible once it is cancelled, applied, or rejected. A current draft or submitted version stays pinned. The outputs below come from a copy of the facility template with deactivate-facility switched to operator-erase.

List the retained Proposals of an Action:

Terminal window
bregctl proposal-retention list \
--runtime-config /etc/breg/runtime.yaml \
--action-id deactivate-facility
Listed the retained Proposals. 1 request.
{"actionId":"deactivate-facility","proposalId":"a491e8e0-27a6-4945-b78b-30e4dfc20cf3","submissionVersion":1,"proposalState":"cancelled","current":true,"retentionMode":"operator-erase","pinned":false,"eligibleForErasure":true,"detailErased":false,"contractFingerprint":null,"effectDigest":null}

Preview the erasure of one exact submission version. The dry run counts what would go and erases nothing:

Terminal window
bregctl proposal-retention dry-run \
--runtime-config /etc/breg/runtime.yaml \
--action-id deactivate-facility \
--proposal-id a491e8e0-27a6-4945-b78b-30e4dfc20cf3 \
--submission-version 1
Previewed the erasure. The request is eligible for erasure. Nothing was erased.
action deactivate-facility
proposal id a491e8e0-27a6-4945-b78b-30e4dfc20cf3
submission version 1
retention mode operator-erase
pinned false
eligible for erasure true
erasure {"proposalSnapshots":0,"targetSnapshots":0,"reviewSubmissionRequests":0,"reviewAcceptedBindings":0,"reviewResultPayloads":0,"applicationReasons":0,"idempotencyResults":2,"draftInputs":1,"attachmentReferences":0}

Run the same selection with erase:

Terminal window
bregctl proposal-retention erase \
--runtime-config /etc/breg/runtime.yaml \
--action-id deactivate-facility \
--proposal-id a491e8e0-27a6-4945-b78b-30e4dfc20cf3 \
--submission-version 1
Erased the retained Proposal.
action deactivate-facility
proposal id a491e8e0-27a6-4945-b78b-30e4dfc20cf3
submission version 1
retention mode operator-erase
erased {"proposalSnapshots":0,"targetSnapshots":0,"reviewSubmissionRequests":0,"reviewAcceptedBindings":0,"reviewResultPayloads":0,"applicationReasons":0,"idempotencyResults":2,"draftInputs":1,"attachmentReferences":0}

Listing again shows "detailErased":true. A pinned version, or an Action in retain mode, is refused and left intact: finish the Proposal under its ordinary authority first. Identical attachment bytes stay while another retained reference needs them. A Review still waiting on remote recovery keeps the exact request and acceptance that recovery needs.

When external storage fails to delete an attachment, the registry keeps the deletion as durable work. A download can already be revoked while the bytes still wait to be deleted.

Terminal window
bregctl proposal-retention cleanup-attachments \
--runtime-config /etc/breg/runtime.yaml
Retried unreferenced attachment cleanup.
pending external deletions 0
external deletion tombstones 0

Run it again while deletions remain pending. If the storage backend or its identity binding is refused, fix that first. Cleanup does not make any Proposal eligible for detail erasure, and your bucket’s own retention policy still applies.

An Action that declares evidence keeps each assertion it accepted: the signed response bytes, the verification context, and the times that bound it. Subject and selector values are never kept. Each kept assertion expires 24 hours after the registry accepted it. Erasing record history does not cover it. The runtime removes expired Action and Proposal Evidence uses every minute in bounded, audited batches. Use the manual command when you need cleanup before the next sweep.

Terminal window
bregctl evidence-retention erase-expired \
--runtime-config /etc/breg/runtime.yaml \
--before 2026-10-16T00:00:00Z
Erased expired action Evidence.
erased 0

The command removes every assertion whose window closed at or before --before, and none still inside its window. A cutoff in the future is refused. The receipts of the Actions that used the assertions stay replayable, so the registry can still account for what it decided. The runtime credential can execute only the fixed expiry function. It cannot read retained assertions or run the manual command, which requires the migration credential.

A successful write holds its response, so a retry with the same Idempotency-Key returns it instead of writing again. Only successes are held. A key is spent per caller across every write, and stays spent for good.

The held response stays available for retention.receiptRetentionDays after the write commits: 7 days unless the runtime file sets a value from 1 to 365. The horizon is fixed at commit, so a changed setting applies to later writes only. Past it, an exact retry answers 410 idempotency.expired and never runs. A key whose response an erasure removed answers 409 idempotency.conflict on either side of the horizon.

Until the drop runs, a spent key also keeps the raw issuer, principal value, and key it was sent under, beside the history it produced. Schedule the drop to match the horizon:

Terminal window
bregctl idempotency-retention erase-expired \
--runtime-config /etc/breg/runtime.yaml \
--before 2026-10-16T00:00:00Z
Dropped expired held idempotency responses; the keys stay spent.
erased 0

The drop removes held responses whose horizon passed at or before --before, together with the raw caller and key, and keeps a digest with the request binding and times. Every key then answers as before: an exact retry 410, a changed retry or an erased key 409. A cutoff in the future is refused, and a second run with the same cutoff drops nothing.

The runtime file’s audit block chooses where the journal goes. The starter project’s example writes a file:

runtime.example.yaml
audit:
hashKeyRef: secret:file/audit-key
path: /var/log/breg/audit.jsonl

The destination is file unless you set destination: stdout. The file rotates at rotateBytes, 100 MiB by default, and sealed segments older than retentionDays, 90 by default, are removed when the writer opens or rotates. Give each runtime instance its own absolute path. Operator commands write to a sibling file, audit.bregctl.jsonl here, on the operator host; collect it with the service’s stream.

Entries carry keyed references and closed vocabulary, never raw record values, and they have no hash chain or signature. Ship sealed segments to append-only storage before they expire, and never edit the active file. Database backups do not contain the journal. How the writer stalls and recovers a torn line is in Audit retention, and how entries pair up is in Base Registry Engine retention.

When a command on this page is refused or waits on the migration lock, see Monitor and recover.