Skip to content
Registry StackDocsDevelopment (unreleased)

Change an active registry

For the operator

View as Markdown

You run a registry that is already active, deployed as deploy a registry describes, and you have edited its project. This page takes the edit to a successor package that the database runs, and covers what to do when an activation fails or a change has to be undone.

A successor goes through the same test, package, plan, and apply steps as the first package, with the active package as its baseline. Each successor names the digest of the package it follows, and each database keeps an activation ledger that bregctl status reads. Paths given to --runtime-config, --package, --backup, and --output must be absolute.

bregctl sorts every change between two packages into a class. A compatible additive change, such as a new optional field, a code added to a vocabulary a field uses, or a widened length bound on a text field, applies with no further evidence. A widened or narrowed grant on a Record type’s access profile is an access change and also applies without review: the review is the holder of the migration credential reading the diff before apply. Every change to an Action needs a reviewed migration, even a changed description, and so does a query inventory change that is more than a route gaining or losing a profile. Removing a field, changing its type, or anything else that can lose or rewrite stored Records needs a reviewed migration too, and a few changes, such as a new project.version, are refused outright: the version is bound to the database for its lifetime.

Compare the candidate with the active package

Section titled “Compare the candidate with the active package”

Compare the edited project with the active package before anything else, so the review covers exactly what the successor would carry. Here the minimal starter’s project changes one Action’s description, and the comparison reads the active package directory:

Terminal window
bregctl diff ./directory --package /srv/registry/build-1/package
Classified the candidate against the baseline. 1 change.
profile authoring
baseline assurance integrity-only
baseline package revision sha256:f55a33146a03b318240ea9329f9707434cf24098910501b083a7ac30bdd5480f
baseline registry revision sha256:1bff3a06d9fbe1a666a3961a48fc5eec8efd8ea4bd0b613ad0fe6d4495d90dfe
candidate registry revision sha256:860bbd3a1f5734e46a7a363dd12b6d426e26b7bedf5da44d0cc7b108b44d2cfe
changes 1
{"classification":"access-change","change":{"class":"access-or-disclosure-change","code":"action-changed","target":{"kind":"action","memberId":"update-contact-email"}}}

--package checks only the package’s integrity, so a reviewer can run it without runtime credentials; the report says integrity-only. Pass --runtime-config instead to compare against the package the database has activated. For an access change the diff also marks each detail as widening, narrowing, or review-required.

Removing a field or a Record type is not erasure. The successor drops the live column or table, but every earlier revision snapshot and every backup still holds the removed values, and the diff warns with diff.history.removed-values-retained. When the removal is for data minimization, also erase the retained history of each affected Record.

When a change needs review, test refuses the candidate and names what to cover:

bregctl test refused.
error migration.review.required reviewedMigrations
the successor cannot be applied automatically; the changes to review are:
action-changed at registry. Run diff, review the migration and its rehearsal
evidence, then provide --reviewed-migrations to both test and package
1 error, 0 findings.

A reviewed migration is a directory you write and keep beside the project. Each migration lives at modules/<module>/migrations/<id>/ and holds a descriptor.json, a rehearsal.json, and any SQL steps, assertions, or fixtures the descriptor names. Every path inside it is relative to the directory root. The descriptor covers exactly the changes diff reported that are not compatible additive, each with its code and target. A change to an Action alone needs no SQL:

reviewed-migrations/modules/directory/migrations/describe-email-update/descriptor.json
{
"apiVersion": "id.registrystack.org/formats/breg/migration-descriptor/v1alpha1",
"kind": "BRegMigrationDescriptor",
"id": "describe-email-update",
"changeClass": "access-or-disclosure-change",
"covers": [
{
"code": "action-changed",
"target": {"kind": "action", "memberId": "update-contact-email"}
}
],
"recovery": "exact-target-resume",
"lockTimeoutMilliseconds": 1000,
"statementTimeoutMilliseconds": 60000,
"steps": [],
"preAssertions": [],
"postAssertions": [],
"rehearsalReceiptPath": "modules/directory/migrations/describe-email-update/rehearsal.json"
}

A migration that rewrites stored Records adds SQL steps scoped to one recordType, pre-assertions and post-assertions that each return one boolean, and, when the change is destructive, a backupBindingPath. Migration descriptor lists every key with its values and bounds.

The rehearsal receipt, rehearsal.json, binds the migration to the exact baseline and candidate: the prior package digest and schema fingerprint, the descriptor’s planDigest, the digest of every SQL file and fixture, the PostgreSQL major version, the row counts your rehearsal observed, and the candidate’s final schema fingerprint. You write it from your own rehearsal; bregctl checks it and does not generate it. Two commands give you the values it needs:

Terminal window
jq -jcS . descriptor.json | shasum -a 256
bregctl test ./directory --fingerprint-only --runtime-config /srv/registry/runtime-test.yaml

The first prints the planDigest (prefix it with sha256:) for a descriptor of ASCII text and integers. The second installs the candidate on the disposable database, reports its schema fingerprint, and rolls back without writing a receipt.

Pass the active package directory as --baseline-package to both test and package, and the reviewed migration directory as --reviewed-migrations when there is one:

Terminal window
bregctl test ./directory \
--baseline-package /srv/registry/build-1/package \
--reviewed-migrations ./reviewed-migrations \
--runtime-config /srv/registry/runtime-test.yaml \
--credentials /srv/registry/schema-test-credentials.yaml \
--output /srv/registry/build-2/schema-test-receipt.json
Fixture run passed. 1 journey.
profile production
registry revision sha256:860bbd3a1f5734e46a7a363dd12b6d426e26b7bedf5da44d0cc7b108b44d2cfe
schema fingerprint sha256:9c2373a0550d2c11dd8514931e2f789367a18666b658370b21bd3875cfcddc62
successful journeys directory-contact

With a baseline, test first rebuilds the predecessor schema on the disposable database, then runs the successor migration over it in the order apply will: pre-assertions, the generated statements, your steps, then post-assertions. It requires the result to reach the candidate’s schema fingerprint, and rolls it all back before the journeys run. A statement PostgreSQL refuses fails test, so package cannot publish a migration apply would reject. The rehearsal runs over empty tables: it does not prove that your data satisfies a step or an assertion. Rehearse that on a restored copy of production. The run uses up its disposable database; recreate it before the next run.

Package the successor with the same two arguments, as deploy and serve describes:

Sealed and published a deployment package.
profile production
package digest sha256:6efe2189c718b7383607b78fd2e3714fc54aaf630b7260e4083ac52dc2146111
registry revision sha256:860bbd3a1f5734e46a7a363dd12b6d426e26b7bedf5da44d0cc7b108b44d2cfe
package files 24

If a source file, a reviewed file, or the baseline changes afterwards, test and package again. A refusal of the reviewed directory ends in -refused and names the part that failed, such as migration.review.coverage-refused for a cover that does not match the diff.

For a successor, the runtime file still names the active package, and the target comes from --package. Run plan first. It takes the apply lock, makes every check apply makes before maintenance begins, then rolls back and records nothing:

Terminal window
bregctl plan --runtime-config /etc/breg/runtime.yaml --package /srv/registry/build-2/package
Planned a successor activation over the active package; nothing was changed. Run `bregctl apply --package DIR` to activate it.
activation successor
package digest sha256:6efe2189c718b7383607b78fd2e3714fc54aaf630b7260e4083ac52dc2146111
registry revision sha256:860bbd3a1f5734e46a7a363dd12b6d426e26b7bedf5da44d0cc7b108b44d2cfe
active package digest sha256:f55a33146a03b318240ea9329f9707434cf24098910501b083a7ac30bdd5480f
role mode split
plan kind reviewed
change count 1
reviewed migration count 1
checks passed successor-binding, migration-role, runtime-write-authority, prerequisites, proposals, history-coverage, webhook-bindings, active-state

apply activates a package only when it was built over the active one. It refuses a package built against another baseline, an older package, or another registry’s package with apply.package.refused, and a runtime file that names another database with apply.database.identity-mismatch. A destructive migration needs a fresh backup of this environment’s database: plan lists it under requiredBackups, and both commands take it as --backup <backupBindingPath>=<binding file>. The binding names the backup file, its digest and size, and when it was taken, at most 31 days ago; the file must be owner-only (0600) with one hard link. Backup binding lists its keys, and a binding that fails a check refuses the apply with apply.backup-evidence.refused before anything changes.

If plan or apply reports apply.hooks.local-retained, the active package stays ready and no target is pinned. Settle local hooks before go-live, then run plan again. This refusal needs no migration reconciliation.

Pass the digest you reviewed as --expected-digest, so a package directory replaced since the review is refused with apply.package.digest-mismatch before any database contact:

Terminal window
bregctl apply --runtime-config /etc/breg/runtime.yaml --package /srv/registry/build-2/package \
--expected-digest sha256:6efe2189c718b7383607b78fd2e3714fc54aaf630b7260e4083ac52dc2146111 \
--operator-reference change-2026-014
Activated the package over its predecessor.
activation successor
package digest sha256:6efe2189c718b7383607b78fd2e3714fc54aaf630b7260e4083ac52dc2146111
schema fingerprint sha256:9c2373a0550d2c11dd8514931e2f789367a18666b658370b21bd3875cfcddc62
activation id b4db0b17-ae38-440b-a908-10096475a58b

Then point the runtime file’s package.root at the successor’s package directory, run bregctl verify --runtime-config /etc/breg/runtime.yaml, and restart the server. The old process does not keep serving: its readiness check sees the database move forward, and GET /ready and Record reads return 503 until it restarts on the successor. bregctl status lists the activation with outcome applied beside every earlier one.

Applying the package that is already active is refused with apply.package.already-active and changes nothing; plan reports pending: false in that case, so a deploy job applies only when plan reports pending: true. An activation also closes every open import authority, because each is bound to the package it opened under; open a new one to continue a load, as move data in bulk describes.

An apply that fails after maintenance begins leaves the database in maintenance with its target pinned. Readiness stays unavailable, a restart does not repair it, and no other successor can activate:

bregctl apply refused.
error apply.migration.failed database
the Registry package apply failed and requires exact-target reconciliation
1 error, 0 findings.

The ordinary answer is to fix the cause and retry the same apply. When that is not possible, ask migration reconcile what the database actually holds. Without --execute it changes nothing:

Terminal window
bregctl migration reconcile --runtime-config /etc/breg/runtime.yaml \
--package /srv/registry/build-3/package --operator-reference change-2026-015
Reconciled the migration. Outcome revertible, 0 steps.
outcome revertible
executed false
maintenance status failed
pinned target digest sha256:71e1800791a91c501d567762119ab15dc16139c19bc9bb09b16f40263989fb0c
active package digest sha256:6efe2189c718b7383607b78fd2e3714fc54aaf630b7260e4083ac52dc2146111
presented target digest sha256:71e1800791a91c501d567762119ab15dc16139c19bc9bb09b16f40263989fb0c
target catalog finding none
active catalog finding none
unresolvable reason none
plan kind reviewed
migration steps 0
reviewed plan closed false
durable step progress false

The outcome is one of five. ready means nothing is pinned. in_progress means another apply or reconcile holds the lock; wait and assess again. completable means every step of the target committed and only the activation is missing. revertible means the target committed no step and the database still matches the active package. unresolvable means the database matches neither package; restore the backup, as the next section describes.

Terminal window
bregctl migration reconcile --runtime-config /etc/breg/runtime.yaml \
--package /srv/registry/build-3/package --operator-reference change-2026-015 --execute

The report repeats the assessment with executed true. Its maintenance status is the state read before the transition, so confirm the result with bregctl status, whose first line reads:

The database runs its active package and is ready.

Abandoning a target closes its ledger row as reverted and leaves the active package in place; you can apply the same package again once its cause is fixed. Completing one writes what a successful apply writes, so update the runtime file and restart as after any activation.

Packages only move forward: the package that was active before is refused with apply.package.refused. When the activated model is wrong but the data is sound, undo the change with a new successor. Restore the earlier project sources, keep project.version, run diff against the active package, and test, package, and apply the result with the active package as --baseline-package. The diff classifies the revert like any other change: undoing an added field is a field removal and needs a reviewed migration and a backup. Records written under the undone model stay, reshaped by the revert’s migration.

When the data itself must go back, because migration reconcile reported unresolvable or a migration changed rows no successor can restore, restore the backup you took before the apply.

Follow back up, restore, and drill recovery for the procedure, including the instance-claim adoption a restored database needs before it serves. Point package.root at the package that was active when the backup was taken, and build the next successor over that package. A restore also brings back history that a later history erase removed, so repeat those erasures before the registry serves.

Restoring Base Registry Engine alone also restores its earlier view of Review work, but cannot undo cancellations already settled by Casework. Compare affected retained rounds with Casework before resuming that work; recover ended rounds through requester Cancel or Rebase and Submit for fresh Review. A database restore does not restore cross-service Review authority.

You can still read and list these submitted Proposals. An automatic application reports blocked, reason contract-changed, recovery correct-input, and automaticRetry.scheduled: false, including when a withdrawn Review received approval before any application job existed. The historical decision remains evidence; the current turn belongs to the requester.

A Proposal reports its applicationStatus with a safe reason and a recovery hint. A missing review authority binding stops only the Proposals that depend on it; restore the exact endpoint, producer, policy, and credential, and never read an authority outage as an approval. When automatic application is blocked with a last error of dependency-outage, source-unavailable, or lease-expired, fix the cause and requeue it:

Terminal window
bregctl review-recovery retry-application --runtime-config /etc/breg/runtime.yaml \
--action-id <action> --proposal-id <proposal-id> --submission-version <version>

The command needs attempts remaining, puts the application back in the queue, and cannot replace a review decision or reset a refusal on the merits.

Resubmit or close a review the authority lost

Section titled “Resubmit or close a review the authority lost”

When the review authority lost or never answered an accepted review, two commands take the same arguments as retry-application. bregctl review-recovery resubmit sends the exact retained review request again under its original idempotency key. It applies when the authority reports the result unknown, when polling ran out of attempts, when submission recovery expired, or after an operator close. bregctl review-recovery close ends the wait for an accepted review without a result, and marks it failed as operator-closed. Neither runs on a withdrawn Proposal, one with a recorded result, or one whose retained request was erased. Fetch the Proposal again afterwards and keep its submissionVersion.

Turn encryption on over existing plaintext

Section titled “Turn encryption on over existing plaintext”

Adding encrypted: true to a field that already holds values is a reviewed migration with a backfill step and a history choice, as encrypt a field that already holds data explains. Around its apply you run two commands.

Before the apply, while the runtime file still names the active package, run the preflight:

Terminal window
bregctl field-encryption preflight --runtime-config /etc/breg/runtime.yaml \
--package /srv/registry/build-4/package

It changes nothing and reports no values. It binds the same predecessor the apply binds, and reports per covered field the rows still holding plaintext and the retained revisions, Proposal snapshots, idempotency responses, and outbox payloads that mention it. When the field has a unique lookup, it names up to 64 Records whose values would collide; correct one of each pair first, since the apply refuses the same collision before it seals anything.

When the history choice is retain-plaintext-history, the apply also refuses while a retained Proposal snapshot still holds the field’s plaintext, with field-encryption.history.retained-proposal-snapshot. Choose erase-and-rebaseline instead and build the package again.

With erase-and-rebaseline, the apply seals the live values but erases nothing. Once the package is active, write an owner-only request file with an operator reference and a reason:

erase-history.json
{
"operatorReference": "change-2026-016",
"reason": "erase plaintext history after the field encryption flip"
}
Terminal window
chmod 600 erase-history.json
Terminal window
bregctl field-encryption erase-history --runtime-config /etc/breg/runtime.yaml \
--request-file /srv/registry/private/erase-history.json

The request names no Record or field: its scope is the recorded erase-and-rebaseline flips, and a file with any other key, or one other users can read, is refused before a connection opens. For each affected Record it uses the same erasure as history erase, then restores snapshot coverage with one rebaseline, which blocks reads and writes until it commits; run it in a maintenance window as restore snapshot coverage describes. Backups taken before the flip still hold plaintext until you retire them.

Codes this page does not cover, and what each asks of you, are on monitor and recover.