Versioned archive. You are viewing v0.38.0. For the latest released guidance, use Latest release. Report archive issues on GitHub.
You operate an active registry under an institutional retention policy, hold the migration credential, and need to erase retained history or change-request detail, restore snapshot coverage after an erasure, or retain the audit stream. Maintenance uses the migration interlock and writes minimized audit entries separately from the database transaction. Reports carry counts rather than values. The erasure and attachment cleanup commands remove bytes that nothing restores. The next section says exactly what each one destroys.
What cannot be undone
Section titled “What cannot be undone”| Command | Removes for good | Leaves in place |
|---|---|---|
history erase | The retained revisions of one record up to a revision number, the correction context and retained outbox payloads that referenced them, and the cached idempotency responses that could replay them. | The current record, every later revision, and the audit journal. |
request-retention erase | The payload detail of one change-request proposal version: proposal and target snapshots, idempotency results, request revision snapshots, outbox payloads, intake rows, and attachment references/content. | The value-free lifecycle rows of that request, and record history. |
request-retention cleanup-attachments | Unreferenced external attachment bytes awaiting deletion, including abandoned uploads and objects recreated after an earlier confirmed deletion. | Live attachment references, request detail, and durable deletion tombstones. |
evidence-retention erase-expired | The signed Evidence responses and verification context a governed action retained, once their 24-hour window has closed. | Every retained assertion still inside its window, and the action receipts that used them. |
history rebaseline, request-retention list, and request-retention dry-run remove nothing:
the rebaseline writes one baseline commit, and the others only read. A saved export or a snapshot
reference is not a way back either:
an erased revision is gone from the registry, and a snapshot reference that named it can no longer
be served.
A package change removes nothing from history either. A successor that drops a field or an entity
leaves the removed values in every revision snapshot recorded before it, and bregctl diff
reports each such removal as diff.history.removed_values_retained. history erase is the only
command that removes them, and it removes whole revisions of one record, not one field.
Every command on this page reads the runtime file and connects to PostgreSQL directly with the
migration credential, and the erasures and the rebaseline take the exclusive registry
lock, so run them from the operator host while no activation is in progress. When the database
uses a private certificate authority, set
SSL_CERT_FILE to its PEM bundle for each command. Paths given to --runtime-config,
--request-file, and --output must be absolute.
Erase retained history
Section titled “Erase retained history”The migration authority can erase the retained revisions of one record up to a revision number. This is irreversible maintenance, not an API permission, and it does not remove the current record. Apply the institution’s live-data removal policy separately.
Write an owner-only JSON request file (mode 0600) so identifiers and reasons never appear in
process arguments:
{ "entityId": "membership-record", "recordId": "00000000-0000-4000-8000-000000000001", "eraseThroughRevision": 2, "operatorReference": "approved-maintenance-001", "reason": "approved-retention-request"}Confirm the record and the approved revision boundary before running the command, and account for
backups, saved exports, and copies already delivered to consumers. A saved snapshot reference
cannot restore erased bytes. There is no dry run. Erasing baseline data also makes snapshot
coverage unavailable until history rebaseline
restores it, and that command holds a write lock on every entity table until it has verified
every live row, so plan the erasure for a window in which writes can wait.
history erase refuses to run without --acknowledge-irreversible, because erasure cannot be
undone and no command restores the erased revisions.
bregctl history erase \ --runtime-config /etc/breg/runtime.yaml \ --request-file /srv/registry/private/erasure.json \ --acknowledge-irreversibleOne transaction erases at most 10,000 revisions, scrubs affected correction context and retained
outbox payloads, and replaces affected cached idempotency responses with refusal tombstones, so a
retried mutation key cannot replay an erased body. The result and the audit record carry counts,
not values. A refusal names its cause without naming the record, and a request file that is not
owner-only is refused before any connection is opened. Snapshot references at or after the
earliest erased commit become unavailable, including every later
snapshot; erasing baseline data makes all snapshot coverage unavailable. Nothing re-baselines the
registry on its own: run history rebaseline when snapshot reads of current state must work again.
Live reads and writes continue.
An erasure of revisions committed after the coverage baseline does not hold back the next
package: bregctl apply accepts a successor when the only gap in coverage is a committed erasure recorded in the
database’s dedicated history-erasure coverage table. Erasing baseline data, or any gap without
that committed coverage record, still refuses every successor package until history rebaseline
restores coverage. Audit-file retention does not change this decision. The
refusal is apply.history.coverage_incomplete, and it leaves maintenance state unchanged, so
apply the same package again once the rebaseline completes.
Restore snapshot coverage after an erasure
Section titled “Restore snapshot coverage after an erasure”After an erasure, a snapshot reference for current state is refused until the migration authority
re-establishes a covered position. history rebaseline is that command, and it runs under the
same interlock as the erasure: the configured migration role, bounded timeouts, the exclusive
registry lock, a ready registry, and minimized audit entries.
Write an owner-only JSON request file (mode 0600) carrying the operator reference alone:
{ "operatorReference": "approved-maintenance-002"}bregctl history rebaseline \ --runtime-config /etc/breg/runtime.yaml \ --request-file /srv/registry/private/rebaseline.jsonOne transaction proves the retained journal head of every live row still reproduces that row, installs one baseline commit at the head, and moves the coverage baseline to it. It restores nothing that was erased: references taken before the new baseline stay unavailable, because the bytes they named are gone. Take a fresh reference afterwards. The command refuses when coverage is already complete, when the registry is not ready, when a retained journal head is not indexed by a commit, and when a live row has no retained journal head that reproduces it. The result and the audit record carry counts and positions, not values.
The transaction has no live-row limit. It reads each entity’s live rows, their journal heads, and the retained heads in the same record-identifier range in pages of 1,000 records, and it proves each entity holds exactly one retained journal head per live row. Memory stays flat as the registry grows, and each statement is bounded by one page of records and their retained revisions, which is not a fixed cost: a page of records that carry thousands of revisions each reads far more than an average page, and the last statement for each entity counts every retained head past its last live row.
The run is a read outage for the whole registry. To read rows under forced row security, the
migration role lifts that force on every entity table before the first page, and lifting it takes
each table exclusively until the transaction commits or rolls back. API reads and writes of every
entity wait for the whole run, and a request that waits past
operationalTimeouts.httpRequestMilliseconds fails. The migration role deliberately has no
BYPASSRLS authority, which is what would let it read without that lock. Plan the run as a
maintenance window:
- Size the window from a rehearsal. Restore a recent backup to a scratch database, erase there,
and time
history rebaselineagainst it. The run grows with the number of live rows and retained revisions across all entities; allow at least twice the rehearsed time. - Stop or drain API traffic for the window, or announce that reads and writes fail during it.
- Set
operationalTimeouts.migrationLockMillisecondslong enough to wait out in-flight requests at the start. SetoperationalTimeouts.migrationStatementMillisecondslong enough for one scan of the whole revision journal: before the entity tables are locked, one statement proves every retained journal head is indexed by a commit. Every later statement reads one page, so the same timeout must also cover the page whose records hold the most retained revisions; the rehearsal shows whether it does.
history.rebaseline.live_rows.unverified reports one live row that its journal head does not
reproduce, and names neither the entity nor the record, because operator refusals here carry no
values. It stops at the first entity whose live rows and journal heads do not line up, and inside
an entity at the first record identifier whose values differ, so a run that gets further has
already proved every earlier entity. To find the row, read the revisions of the records you
suspect with GET /v1/records/{route}/{recordId}/revisions, or compare live rows with their
journal heads directly under the migration role you already hold. The usual cause is a live row
that changed outside the server’s write path, so the journal never recorded the change.
Erase change-request detail
Section titled “Erase change-request detail”Change-request proposals and application receipts keep their own retention. A request entity
whose changeRequest.retention.mode is operator_erase allows the operator to erase the payload
detail of one proposal version while keeping its value-free lifecycle rows; the default retain
mode refuses erasure. List the retained rows, then count what one erasure would remove:
bregctl --format json request-retention list \ --runtime-config /etc/breg/runtime.yaml \ --request-entity placement-correction-request --limit 50
bregctl --format json request-retention dry-run \ --runtime-config /etc/breg/runtime.yaml \ --request-entity placement-correction-request \ --request-id "<requestId>" --proposal-version 3Each list row reports the request state, whether the version is current or pinned, its retention
mode, eligibleForErasure, and whether detail is already erased; a listing longer than --limit
continues from the cursor it returns, passed as --after-cursor. The dry run reports the counts of
proposal snapshots, target snapshots, idempotency results, request revision snapshots, outbox
payloads, intake rows, and attachmentReferences that the erasure would remove,
and changes nothing. Canceled drafts are eligible under the same policy even when
no proposal was submitted.
Erase only a version the dry run counted and the retention request approves. The erasure keeps the lifecycle rows, so the request stays visible with its state, but the proposal and target values are gone from the registry. Attachment access for that version is also revoked.
bregctl --format json request-retention erase \ --runtime-config /etc/breg/runtime.yaml \ --request-entity placement-correction-request \ --request-id "<requestId>" --proposal-version 3The erasure reports the same counts as the dry run. A refusal, request_retention.operation.refused,
names the cause without values: the entity’s retention mode is retain, or the version is pinned.
Erasing record history does not erase these rows, and erasing these rows does not touch record
history.
Attachment bytes are removed after their final retained reference is erased.
Other requests or proposal versions that legitimately retain identical content
keep their bytes and access. Erased slot metadata keeps filled state, size, and
hash while dropping content type, verification status, and upload actor/time.
S3 deletion failures remain in a durable retry queue; pendingExternalDeletions reports the registry-wide
outstanding count. Use attachment cleanup to
retry these deletions independently of request erasure. A nonzero
count means physical deletion is still pending, even though scoped download
access has been revoked. externalDeletionTombstones counts confirmed S3 deletions
retained for future absence checks. Continue periodic cleanup runs to recheck these
keys and remove any orphan bytes from a delayed remote write. Confirmed tombstones
also remain subject to the first-upload storage and verification binding.
Configure backup and replication expiry separately, and keep the bucket versioning
rules in Attachment storage.
An external verifier may retain its own copies or logs; registry erasure does not
remove them. Set that service’s retention policy as part of External attachment
verification.
Retry external attachment cleanup
Section titled “Retry external attachment cleanup”Use request-retention cleanup-attachments when S3 deletions remain pending or
when an interrupted upload left unreferenced content. Cleanup works even when all
requests use retain mode or the registry has only active drafts. It uses the
migration credential and verified runtime configuration, including the pinned
storage and verification bindings.
Check that the runtime configuration identifies the intended registry. Cleanup preserves live attachment references and request detail; it permanently removes unreferenced external bytes selected by the bounded cleanup pass.
bregctl --format json request-retention cleanup-attachments \ --runtime-config /etc/breg/runtime.yamlThe report contains the registry-wide pendingExternalDeletions and
externalDeletionTombstones counts. Repeat cleanup while deletions remain pending,
and continue periodic runs to recheck confirmed tombstones for delayed remote
writes. A backend failure leaves work durable for a later run. A configuration or
identity refusal requires restoring the matching runtime binding before retrying.
This command does not make an active draft or a retained request eligible for
detail erasure.
Erase expired Evidence uses
Section titled “Erase expired Evidence uses”A governed action that calls a verified Evidence capability retains what it accepted. Under handler
ABI registry.action-handler/v2, each accepted assertion is stored beside the applied action: the
signed response bytes, the verification context, the capability, the contract fingerprint, the trust
binding, the assertion identifier, its issue, observation, and validity times, the moment the
registry accepted it, and the declared maximum observation age. Subject and selector values are
never retained. The runtime role holds an insert-only grant on that state, so no request path reads
or deletes what it wrote.
This is a separate retention scope with its own clock. Each retained assertion expires 24 hours after the registry accepted it, erasing record history does not cover it, and nothing removes the expired rows on its own. Schedule the erasure on the operator host, under the migration authority and the exclusive registry lock the rest of this page uses.
The erasure removes the retained signed responses and verification context for good, and takes no copy first. Export what the institution’s retention policy requires to keep before the window closes. The action receipts that used the assertions stay replayable, so the registry can still account for what it decided.
bregctl evidence-retention erase-expired \ --runtime-config /etc/breg/runtime.yaml \ --before 2026-01-01T00:00:00ZThe command removes every row whose 24-hour window closed at or before the cutoff, and no row still
inside its window: a cutoff later than the database transaction time reaches only expired material,
and a cutoff in the future is refused. Both paths must be absolute. The report carries a count and
no values. The run verifies the expected registry identity, catalog, and readiness while it holds
the registry transaction lock through the deletion, and the configured runtime credential cannot
perform the deletion at all. Unlike request-retention erase, which erases one exact proposal
version, this erasure works from the time boundary alone.
Keep the audit journal
Section titled “Keep the audit journal”Each audited request writes one request entry before protected I/O and at least one response
entry before disclosure or after a mutation commits. The entries share a correlation and contain
keyed references and closed vocabulary, not raw record values. A refusal can have only a response
entry. The file destination acknowledges an append after fsync; stdout flushes each line and
provides best-effort delivery. A failed request entry blocks protected I/O, and a failed response
entry blocks disclosure. A request that ends before its outcome is known, because it failed, timed
out, or its caller went away, or an operator command that exits early, still answers its request
entry. A runtime read or mutation answers with a response entry whose record phase is
unfinished, carrying only the operation and request it answers; an operator command answers with a
terminal response whose outcome is unfinished. An operator command also answers unfinished
when it is refused, or when it stops with nothing to do, such as a history rebaseline with no
history to rebaseline, so an unfinished outcome does not always mean the outcome was unknown.
This pairing is a property of a running process, and a request may be answered more than once, so read every response under a correlation. A process that crashes, is killed, exits, or shuts its runtime down while a write is in flight, or a destination that already stopped accepting entries, can leave a request entry without a response.
Operator commands follow the same rule. evidence-retention erase-expired records the cutoff it
was given in its request entry and the number of assertions it erased in its response.
request-retention erase deletes the external attachment objects before it records its response,
which states how many objects still wait for deletion; when that deletion pass itself fails, the
count is null and the command reports the failure. If the erasure committed but its response
entry could not be written, the command reports request_retention.erasure.unaudited: the detail is
gone, so restore the audit destination and reconcile the erased request against the database. An
event delivery’s attempt is recorded inside the transaction that leases it, before its request
leaves. If that lease transaction does not commit, no request leaves and the attempt is answered
worker_interrupted, so an attempt entry does not prove a request was sent. An attempt’s terminal
outcome is recorded only once the delivery’s state has committed. When that commit fails and whether
it committed cannot be read back, the attempt is answered worker_interrupted with disposition
unknown, which claims no delivery state; read the delivery’s state to learn it. Lease-expiry
recovery answers the attempt again if its lease survived. An operator replay is recorded as a request
before the reset and a response once it commits or is refused, or replay_unfinished when its
commit failed and whether the reset committed cannot be read back; read the delivery’s state before
replaying it again.
A crash or a killed process can also tear the file’s final line itself, leaving it without its
closing newline. The next open of that destination, by the runtime or a bregctl companion command
on its own sibling file, moves the torn bytes to the owner-only side file <audit.path>.torn,
truncates the active file to its last complete line, and logs the side file’s path and byte count.
No acknowledged entry is lost, because an entry is acknowledged only after it is synced. Archive the
side file with the sealed files and remove it: the writer never overwrites it, so a second torn line
is refused until the first side file is moved. Do not edit the active file to repair it, since that
is the same external change the writer already treats as a fault (see
Audit retention).
A mutation can already be committed when its response entry fails. Use the operation’s idempotent
retry where one exists. Ingestion-run creation has none, and it returns no run id when this
happens: once the runtime serves again, list the open ingestion runs for the input’s digest and
resume or cancel the one you find. An operator rerun of history erase, history rebaseline, or
migration reconcile finds the state the first run committed instead of replaying its result, so
read what that rerun reports before doing anything else.
Configure the stream in the runtime file:
audit: hashKeyRef: secret:file/audit-key destination: file path: /var/lib/breg/audit/breg.jsonl rotateBytes: 104857600 retainDays: 90The file path is absolute and belongs to one process. Give each runtime instance its own path;
bregctl maintenance uses a sibling such as breg.bregctl.jsonl on the operator host. Collect
those companion streams as well as the service stream. Local development keeps its files under
.breg/dev/audit, including after bregctl dev stop --remove.
The writer rotates at rotateBytes and removes sealed segments older than retainDays when it
opens or rotates. Ship sealed segments to append-only storage before expiry and verify receipt in
your log pipeline. Do not rename, truncate, or replace the active file or its lock while the writer
runs. See Audit retention for shipping and
recovery. The log has no hash chain or signature; tamper evidence and completeness depend on that
external storage. Database backups do not contain these streams.
Archive any audit records your retention policy requires before upgrading. The database audit
tables and the bregctl audit command group are removed; the upgrade does not export their data.
Choose a fresh audit path when changing from another audit format, and preserve old segments
outside the new writer’s retention directory.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Next move |
|---|---|
A saved snapshot returns 503 source.unavailable | Retained history no longer covers it, after an erasure or an incompatible successor. Take a fresh reference. |
| A fresh snapshot reference is refused after an erasure | Coverage ends before the erased commit. Run history rebaseline to cover the current state again; earlier references stay unavailable. Reads and writes of every entity wait while it runs; see the outage note. |
history rebaseline reports history.rebaseline.live_rows.unverified | A live row and its journal head disagree, and the refusal names no record. Read the revisions of the records you suspect, or compare live rows with their journal heads under the migration role. |
history erase reports history.erase.acknowledgement.required | The command was run without --acknowledge-irreversible. Confirm the record and the revision boundary, then run it again with the flag. Nothing was read or erased. |
history.erase.request_file.refused or history.rebaseline.request_file.refused | The request file must be an absolute path to an owner-only regular file, not a symbolic link, holding the documented fields and nothing else. |
request-retention erase is refused | The request entity’s retention mode is retain, or the version is pinned. Run the dry run for the exact version first. |
evidence-retention erase-expired removes nothing | No retained assertion has passed its 24-hour window yet, or the cutoff precedes every expiry. The count is the whole report. |
| Readiness or an operation reports audit unavailable | Check destination permissions, free space, and the single-writer lock. Repair the destination and restart a stopped writer. For a mutation whose response failed after commit, inspect its receipt or domain history before retrying. |
| An operator command cannot reach PostgreSQL | Check the migration URL secret, the role names, and SSL_CERT_FILE for a private authority. |
- Retention and persistent state for the retention procedures of the other Registry Stack runtimes.
- Harden a production deployment for the controls around the migration credential and the secret root these commands rely on.
- Security overview for how audit integrity and data minimization fit the stack’s security model.
- Change an active registry for the backup every activation needs and the audit records reconciliation writes.