Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/llms.txt
Full corpus: https://docs.registrystack.org/llms-full.txt

# Change an active registry

> Compare an edited project with the active package, test and sign the successor against the active baseline, activate it with reviewed migration evidence, recover a registry that a failed activation pinned, and undo a change by rolling forward or restoring a backup.

You operate an active registry, deployed as [deploy a registry](../breg/) describes, and hold an
edited project or a candidate a signer is ready to approve. At the end of this page the successor
package is active, the runtime file names it, and the process serves it; or a failed activation has
been assessed and resolved without editing migration state by hand.

A successor goes through the same test, package, sign, and apply sequence as the first package, with
the active package as its baseline. The extra work is the comparison before you start, the migration
evidence a reviewed change needs, and the recovery path when an activation fails. Every command reads
`--help`, and paths given to `--runtime-config`, `--package`, `--backup`, and `--output` must be
absolute.

## Compare the candidate with the active package

Start by comparing the edited project with the package the runtime file names, so the review covers
exactly the changes the successor would carry:

```sh
bregctl --format json diff ./my-registry \
  --runtime-config /etc/breg/runtime.yaml
```

`--package <directory>` in place of `--runtime-config` takes a closed package directory as the
baseline and checks only its integrity, so a signer can review a candidate without runtime
credentials or activation authority; the report labels that baseline `integrity_only` rather than
`runtime_bound`.

The diff lists model, access, and route changes with their change codes and, for access changes,
their direction: a scope, purpose, row-boundary, field, count, history, or export change is marked
as widening or narrowing, and mixed changes are marked for review. A change to consent,
recipients, or a consent-issuing action carries a `reason` when it alters who holds an existing
consent, such as a client added to a recipient organization or a gated profile widened in place.
The `reason` is a review aid in the diff output, not a runtime guarantee: the successor enforces
exactly what its package declares. Set `package.sequence` in
`registry.yaml` to the next sequence. `registry.version` is bound to the database for its
lifetime: a package that changes it can only initialize a new database.

{/* Evidence: crates/registry-bregctl/src/lib.rs, DiffArgs;
    crates/registry-breg/src/tooling.rs, AccessChangeDirection and GATED_SCOPE_WIDENED;
    crates/registry-breg/src/migration.rs, apply_verified_package(). */}

## Test and package the successor

Pass the active runtime file as `--baseline-runtime-config` to both `test` and `package`, so the
receipt and the package bind to the baseline they will succeed. A change the migration planner
classifies as compatible additive, such as a new optional field or a code added to a vocabulary a
field uses (`field_vocabulary_codes_added`), needs no further evidence. The diff classifies a field
code addition as `lock_or_rewrite_risk`, because the migration replaces the column check under an
exclusive table lock and validates every stored row. A widened grant is an access change: the
planner accepts it as metadata-only, but it still needs a reviewed migration descriptor and
rehearsal evidence, as the metadata-only case below describes.

An action input that starts accepting more codes is additive (`action_vocabulary_codes_added`)
only when every code it gained was added to its vocabulary in the same successor. An input that
starts accepting a code its vocabulary already had, such as a withdraw action that also accepts
`given`, is `action_changed` and needs a reviewed migration. A predecessor package built before
the engine recorded each input's vocabulary cannot show that a code is new, so every input
widening against it is reviewed. An additive input widening is still marked for review in the
diff, because the action accepts the new codes with no further check.
When `test` or `package` reports `migration.review.required`, prepare a reviewed migration
directory and pass it to both commands as `--reviewed-migrations`:

```text
reviewed-migrations/
  modules/<module-id>/migrations/<migration-id>/
    descriptor.json
    rehearsal.json
    steps/<step-id>.sql             when the descriptor declares SQL steps
    assertions/<assertion-id>.sql   when the descriptor declares assertions
    fixtures/<fixture-id>.jsonl     when the rehearsal references fixtures
    backup.json                     for destructive changes
```

The descriptor names the change class, the exact change codes and targets from `diff --format
json`, timeouts, recovery mode, and its evidence paths. The rehearsal receipt binds the prior
package and schema, the descriptor and SQL hashes, the measured target schema, and the timeout
and recovery proofs. Your rehearsal process produces that evidence against the exact candidate;
the command line validates it and does not generate it. The only worked example of the exact
descriptor and rehearsal receipt fields is the review construction in
[`test-adopter-workflow.sh`](https://github.com/registrystack/registry-stack/blob/v0.26.1/products/breg/scripts/test-adopter-workflow.sh),
which builds both documents for a metadata-only access change and passes them to `test`. JSON
artifacts use canonical JSON: sorted keys, no extra whitespace. Keep only referenced files in the directory; symbolic links and
unrelated files are refused. A metadata-only change can declare empty steps and assertions and
still needs rehearsal evidence. A field added as required is created without its constraint, your
reviewed steps backfill it, and the plan constrains the column afterwards.

```sh
bregctl --format json test ./my-registry \
  --baseline-runtime-config /etc/breg/runtime.yaml \
  --reviewed-migrations ./reviewed-migrations \
  --runtime-config /srv/registry/runtime-test.yaml \
  --credentials /srv/registry/schema-test-credentials.yaml \
  --database-id civil-registry-db-1 \
  --signature-threshold 1 --signature-key-id registry-signer-2026 \
  --output /srv/registry/schema-test-receipt-2.json
```

Package and sign as [deploy a registry](../breg/) describes, with the same
`--baseline-runtime-config` and `--reviewed-migrations` arguments. If any source, reviewed
artifact, baseline, or signature-policy input changes afterwards, repeat the test and the signing.
A refusal names its cause. A code ending in `_refused` names the part of the reviewed directory
that failed (the troubleshooting table below lists each); `migration.review.refused` means a
precondition such as the prior revision or the baseline binding failed first; for
`migration.review.fingerprint_mismatch`, rehearse the exact candidate again.
Reviewed artifacts cannot authorize a change the planner classifies as unsupported.

{/* Evidence: crates/registry-bregctl/src/lib.rs, PackageCandidateArgs;
    crates/registry-bregctl/src/reviewed_migrations.rs;
    crates/registry-breg/src/migration_plan.rs;
    crates/registry-breg/src/immediate_actions.rs, contract_only_adds_vocabulary_codes();
    crates/registry-breg/src/tooling.rs, classify_change();
    crates/registry-breg/tests/action_vocabulary_codes.rs;
    crates/registry-bregctl/tests/cli/reviewed_migrations.rs;
    products/breg/scripts/test-adopter-workflow.sh. */}

## Activate the successor

For a successor, the runtime file given to `apply` still names the active package; the target
comes from `--package`. A destructive change also needs its reviewed backup artifact, bound by its
descriptor path:

```sh
bregctl --format json apply \
  --runtime-config /etc/breg/runtime.yaml \
  --package /srv/registry/build-2/package \
  --backup modules/core/migrations/retire-legacy-field/backup.json=/srv/registry/backups/retire-legacy-field.dump
```

:::caution[A failed activation pins its target]
Activation enters maintenance and may change persistent data, which is why a destructive change
carries a reviewed backup. If `apply` fails after maintenance begins, readiness stays
unavailable, a restart does not repair it, and the target stays pinned: no other successor can
activate until that target is resolved. Resolving the reported cause and retrying the same target
package is the ordinary answer. When it is not available, `migration reconcile` reports what the
database actually holds and names the one transition that is safe. Do not run generated SQL by
hand or edit migration state. Take a backup before every `apply`. A database that cannot be
reached before maintenance begins reports `apply.database.unavailable` instead; nothing changed,
so retry the same apply once the database is reachable.
:::

A successful report carries the activated revision, sequence, and schema fingerprint. Update the
runtime file to the successor's package directory, revision, and sequence, run
`bregctl verify --runtime-config /etc/breg/runtime.yaml`, and restart the server promptly. Editing
the runtime file does not change the active process, but the still-running old process does not
keep serving the package it started with either: its readiness check re-verifies the database's
active identity on every poll, and once the successor's `apply` moves that identity forward, the
check fails. `GET /ready` and record reads return 503 and the process logs repeated claim
refusals until you restart it onto the successor package.

Applying the package that is already active is a no-op, so a deploy job can run `apply` again
after the runtime file names the successor. `apply` verifies the package in full as the configured
active package, then confirms under the migration lock that the database records exactly that
package, by revision digest, schema fingerprint, sequence, and deployment identity, as active with
no maintenance pending. It exits 0 with `activation: already_active` and changes nothing. Another
package that only shares the active sequence still refuses with `apply.package.binding_mismatch`.

{/* Evidence: crates/registry-bregctl/src/lib.rs, ApplyArgs;
    crates/registry-bregctl/src/apply_lifecycle.rs, confirm_already_active();
    crates/registry-breg/src/migration.rs, apply_verified_package(), confirm_active_package();
    crates/registry-breg/tests/postgres_migration.rs, real_postgres_confirms_only_the_exact_active_ready_package;
    crates/registry-breg/src/postgres/interlock.rs;
    crates/registry-breg/tests/postgres_migration.rs. */}

## Recover a failed activation

`migration reconcile` assesses a pinned registry and changes nothing without `--execute`:

```sh
bregctl --format json migration reconcile \
  --runtime-config /etc/breg/runtime.yaml \
  --package /srv/registry/build-2/package \
  --operator-reference "<change reference>"
```

It takes the same exclusive migration lock an activation takes, so it never reads a half-applied
package, and it compares the live managed catalog against both the pinned target's expected catalog
and the active package's using the verification an activation performs. It reports exactly one
outcome:

| Outcome | What the database holds | Next move |
| --- | --- | --- |
| `ready` | Nothing is pinned. | Nothing to reconcile. |
| `in_progress` | Another session holds the migration lock. | Wait for that activation to finish, then assess again. |
| `completable` | Every durable step of the pinned target succeeded and the catalog is exactly the target's. Only the activation transition is missing. | Rerun with `--execute` to finish the activation. |
| `revertible` | The pinned target reached no durable step and the catalog is still exactly the active package's. | Rerun with `--execute` to abandon the target. |
| `unresolvable` | The catalog matches neither package, or a reviewed migration committed steps only the same target can finish. | Restore the pre-activation backup. The assessment exits as a refusal, `migration.reconcile.outcome.unresolvable`, whose message names the reason and the catalog finding for each package. |

`--execute` performs only the transition the assessment named, refuses every other outcome, and
records the transition in the audit chain inside that transition's own transaction. Completing an
activation writes the same ledger, history baseline, and postconditions a successful `apply`
writes, so update the runtime
file and restart as you would after any activation. Abandoning a target clears the pin and leaves
the active revision untouched, and it retires that target revision: the abandoned package keeps
its failed ledger row, so a later attempt needs a newly built package revision.

{/* Evidence: crates/registry-bregctl/src/lib.rs, MigrationCommand and MigrationReconcileArgs;
    crates/registry-breg/src/migration_reconcile.rs;
    crates/registry-bregctl/src/reconcile_lifecycle.rs;
    crates/registry-breg/src/postgres/interlock.rs. */}

## Roll back by rolling forward

Packages apply forward only. A target package must carry a higher `package.sequence` than the
active package and name the active revision as its prior, so `apply` refuses an older package
with `apply.package.older_than_active` before it opens a database connection. There are two ways
back, and which one fits depends on whether the data must return too.

**Roll forward to undo a model change.** When the activated model is wrong but the data it holds
is sound, build a successor that reverts the change:

1. Restore the earlier project sources, for example from version control, and set
   `package.sequence` in `registry.yaml` to the sequence after the active one. `registry.version`
   stays as it is.
2. Run `diff` against the active runtime file. The planner classifies the revert like any other
   change: undoing an added field is a field removal, which is destructive and needs a reviewed
   migration and its backup artifact; undoing a widened grant is an access change that needs
   reviewed evidence.
3. Test, package, sign, and apply the successor with `--baseline-runtime-config` as the sections
   above describe, then update the runtime file and restart.

The revert is a new revision with its own ledger row, audit record, and history baseline. Records
written under the undone model stay, reshaped by the revert's own migration.

**Restore the pre-activation backup** when the data itself must go back: `migration reconcile`
reported `unresolvable`, or a reviewed migration changed rows in a way no successor can undo.

1. Stop the server, and export the audit journal with
   [`audit export`](../breg-retention/#keep-the-audit-journal) so the records written
   after the backup are kept outside the database.
2. Restore the database from the backup you took before the `apply`, with your PostgreSQL tooling.
3. Point the runtime file at the package that was active when the backup was taken, with its
   directory, revision, and sequence, run `bregctl verify`, and start the server.

A restore discards every write, activation, and audit record committed after the backup, and it
brings back history that a later `history erase` removed: repeat those erasures before the
registry serves traffic again. Webhook deliveries and exports already sent are not recalled. The
next successor needs a sequence above the restored active package, not above the discarded one.

{/* Evidence: crates/registry-breg/src/package.rs, validate_bindings() and PackageError::OlderThanActive;
    crates/registry-bregctl/src/lib.rs, apply_lifecycle_failure();
    crates/registry-breg/src/package.rs, CompiledRegistryChangeCode::FieldRemoved;
    crates/registry-breg/tests/postgres_package.rs, activation_refuses_an_older_package_as_a_rollback;
    crates/registry-bregctl/tests/cli/reviewed_migrations.rs,
    apply_refuses_an_older_package_and_points_at_the_roll_forward_procedure. */}

## Inspect the migration plan

`migration explain --runtime-config <file>` describes the configured package's migration plan
without executing it; it does not report live database status. New schema fingerprints identify
columns by name, so a fresh installation and an in-place upgrade match even when PostgreSQL stores
columns in a different order. A package measured with the updated algorithm needs the updated
server binary, and earlier packages keep verifying under their recorded rules.

{/* Evidence: crates/registry-bregctl/src/lib.rs, MigrationExplainArgs;
    crates/registry-breg/src/package.rs;
    crates/registry-breg/src/postgres/catalog.rs, CatalogFingerprintVersion. */}

## Troubleshooting

| Symptom | Next move |
| --- | --- |
| `diff` reports the baseline as `integrity_only` | You passed `--package`. That is the signer's view; pass `--runtime-config` for the comparison against the activated package and its trust. |
| `test` or `package` reports `migration.review.required` | The planner classified a change as needing review. Rehearse it, write the reviewed migration directory, and pass `--reviewed-migrations` to both commands. |
| `test` or `package` reports `migration.review.descriptor_refused` | A descriptor is malformed, for example an empty or duplicate `covers` entry, a missing or unknown field-encryption history choice, a chunk size beyond the commit budget, or a plaintext drop ordered before its sealing backfill. Fix the descriptor, then repeat test and package. |
| `test` or `package` reports `migration.review.coverage_refused` | The directory does not cover exactly the reviewed changes: a change is uncovered, a cover names a change the candidate does not make or the wrong change class, a fixture is not bound to any step, or the change is one reviewed artifacts cannot authorize. |
| `test` or `package` reports `migration.review.sql_refused` | A step or assertion uses SQL outside the reviewed subset, for example more than one statement, transaction or role control, an unqualified or undeclared object, or DML without affected-row bounds. |
| `test` or `package` reports `migration.review.evidence_refused` | The rehearsal receipt, external backup binding, or fixture bytes do not match this candidate, baseline, and database. Rehearse again and copy the new receipt. |
| `test` or `package` reports `migration.review.closure_refused` | The set of files does not close: a descriptor the directory names is missing, a path appears twice, or there are too many artifacts. |
| `test` or `package` reports `migration.review.refused` | A precondition failed before the reviewed directory was read, such as the prior revision or the baseline binding. Check the baseline arguments and the fingerprints in the rehearsal receipt. |
| `test` or `package` reports `migration.review.fingerprint_mismatch` | The rehearsal receipt was taken for a different candidate or baseline. Rehearse the exact candidate again. |
| `apply` reports `apply.migration.failed` | The apply failed after maintenance began. Read the report, fix the cause, and retry the same target. If that is not available, run `migration reconcile` to assess the pinned target, execute the transition it names, and restore the pre-activation backup only when it reports `unresolvable`. |
| `apply` reports `apply.database.unavailable` | The migration database could not be reached, or another apply held the migration lock past the lock timeout, before maintenance began. Check that the database is reachable and accepts the migration role, then retry the same apply. Nothing was changed, so there is nothing to reconcile. |
| `apply` reports `apply.package.older_than_active` | The target package is older than the active one, and packages apply forward only. Build a successor that reverts the change, or restore the pre-activation backup, as [roll back by rolling forward](#roll-back-by-rolling-forward) describes. Nothing was changed. |
| `apply` reports `apply.package.empty_plan` | The successor has nothing to apply: its plan has no schema statement and no reviewed migration, and it is not an access or disclosure change alone, for example a successor built from unchanged sources with only `package.sequence` raised. Keep the active package until the model changes, then build the successor from that change. Nothing was changed. |
| `apply` reports `apply.package.binding_mismatch` | The package, or the active package at `package.root`, is bound to a different deployment than the runtime file. The report path names the runtime key that differs (`identity.environment`, `identity.databaseInitializationEnvironment`, `identity.instanceId`, `identity.databaseId`, `package.compilerSourceRevision`, `package.activeRevision`, or `package.activeSequence`) and never either value. A `package.activeSequence` mismatch means the target is a different package at the active sequence: build it as a successor of the active package. Nothing was changed. |
| `apply` reports `apply.package.active_mismatch` | The runtime file names the target as the active package, but the database does not record it as active and ready. Set `package.activeRevision` and `package.activeSequence` to the package the database runs and apply again. If the database has never been activated, apply with `--initial`. If the database is pinned in maintenance, run `migration reconcile`. Nothing was changed. |
| `apply` reports `apply.history.coverage_incomplete` | Retained history coverage does not admit a successor: a `field-encryption erase-history` run has not finished, or an erasure reached the coverage baseline or left a gap the audit journal does not record. Finish the erase-history run, or run [`history rebaseline`](../breg-retention/#restore-snapshot-coverage-after-an-erasure), then apply the same package again. Maintenance state was not changed, so there is nothing to reconcile. |
| `migration reconcile` reports `in_progress` | Another session holds the migration lock. Wait for it to finish, then assess again. |
| The process still serves the old package after `apply` | Activation changes the database, not the process. Update the runtime file and restart the server. |

## Next

- [Review changes before updating a registry](../../tutorials/review-registry-changes/) to
  practise `diff` and a successor on a local registry.
- [Deploy a registry](../breg/) for the test, package, sign, and activate commands the successor
  reuses.
- [Retain, erase, and audit](../breg-retention/) for the audit journal every activation and
  reconciliation writes to.
- [Base Registry Engine configuration reference](../../reference/breg-configuration/) for the
  `package` keys the runtime file binds.