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

# Change an active registry

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

You operate an active registry, deployed as [deploy a registry](../breg/) describes, and hold an
edited project or a reviewed candidate. 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, plan, and apply sequence as the first package,
with the active package as its baseline. Two records keep it in order. The package chain is in
the packages themselves: each successor names the digest of the package it follows. The
activation history is in each database: its activation ledger records every activation that
database has run, and `bregctl status` reads it. One package promoted through several
environments is one link of the chain and one ledger row in each environment's database. 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 reviewer can inspect 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. Replacing a `batch` grant with `import`, which a successor that
puts an entity under change control needs, removes the entity's `:batch` route, and the diff
reports it as a route removal; an entity whose loader already held `import` keeps the same route
when change control is added.

Removing a field or an entity is not erasure. The successor drops the live column or table, but
every revision snapshot recorded before it still holds the removed values in
`registry_internal.registry_revisions`, and so does every database backup. The diff reports each
removal as `diff.history.removed_values_retained` so the reviewer sees this before packaging. When
the removal is for data minimization or a legal reason, erase the retained revisions of each
affected record with [`history erase`](../breg-retention/#erase-retained-history) as well; there is
no command that removes one field from history and keeps the rest of each snapshot.

`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 and removed_value_findings();
    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 package directory as `--baseline-package` 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`), a higher `maxLength` on a `text` field, or a lower
`minLength` on a `string` field under the same `maxLength` (both `field_length_widened`), needs no
further evidence. The diff classifies these as `lock_or_rewrite_risk`, because the migration
replaces the column check under an exclusive table lock and validates every stored row; a
`minLength` lowered to 0 drops the check instead. A `string` field's `maxLength` is its column type,
so raising it is `field_type_changed` and needs a reviewed migration, as does lowering any
`maxLength`, raising a `minLength`, or widening a `decimal`. An action that sets or requires the
entity is reported as `action_target_fields_widened` when a relaxed length bound is its only
change, and stays additive. 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
```

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 digest 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
`products/breg/scripts/test-adopter-workflow.sh` at the release tag you operate,
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-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/schema-test-receipt-2.json
```

### Reviewed migration files

Every file is canonical JSON or SQL, and every path inside a descriptor is relative to the
reviewed directory root, starting with `modules/<module-id>/migrations/<migration-id>/`.

- `descriptor.json` holds `id`, `changeClass`, `covers` (each change `code` and `target`
  from `diff --format json` that is not compatible additive), `recovery` (`exact_target_resume`),
  `lockTimeoutMs`, `statementTimeoutMs`, `steps`, `preAssertions`, `postAssertions`,
  `rehearsalReceiptPath`, and, for a destructive change, `backupBindingPath`.
- A step is one of three kinds. `transactional_sql` runs one statement from `sqlPath` in its own
  transaction, which also records the step's ledger progress, so a later step or post-assertion
  that fails does not roll it back and `apply` resumes after it; DML must declare `affectedRows`
  bounds, and every row it changes gets a history revision. `chunked_backfill` runs one `UPDATE` of its declared entity once per chunk of
  record identifiers, bound as a UUID array, within `chunkSize` (at most 1,000) and
  `maxTotalRows`; each chunk commits on its own and appends one history revision for every row it
  changed, in one history commit, so snapshot and as-of reads agree with the backfilled rows.
  `field_encryption_backfill` carries no SQL; the engine seals the covered fields chunk by chunk
  and journals each chunk the same way. A journaled step is one `UPDATE`, after any leading
  comments, and may not name a record metadata column (`record_revision`, `record_lifecycle`,
  `active_package_revision`, `created_at`, `updated_at`) anywhere in the statement, even to read
  it. Outside comments and plain string literals, its text may not contain a second statement or
  another statement word such as `drop` or `delete`, and a `transactional_sql` step may not name
  `record_id`. A dollar-quoted body or a literal holding a backslash is read as written, so a
  statement word inside it refuses the step. A Unicode-escape identifier or string (`U&"..."`,
  `U&'...'`, with or without `UESCAPE`) refuses the step wherever it appears. The journal also
  refuses a step that changed any record metadata column.
  Each step lists the `objects` (schema, table, entity, kind, member, physical name) its SQL may
  touch.
- An assertion is one `SELECT` returning exactly one boolean column. Pre-assertions run against
  the predecessor tables before any step, post-assertions against the successor tables after
  every step, and activation stops on a false result.
- `rehearsal.json` binds the prior package digest and schema fingerprint, the descriptor and SQL
  digests, the fixture inventory, the PostgreSQL major version, the affected row counts your
  rehearsal observed, the final schema fingerprint, and the lock-timeout and resume proofs.
A destructive descriptor's `backupBindingPath` is `modules/<module-id>/migrations/<migration-id>/backup.json`,
but that file is not part of the reviewed directory or the package. It names the backup binding
each environment supplies at activation, because every environment takes its own backup of its
own database. The binding is a JSON document you write beside the backup, outside the package,
and pass to `plan` and `apply` as `--backup <backupBindingPath>=<absolute binding file>`. It holds
exactly these keys: `databaseId`, `priorPackageDigest`, `priorSchemaFingerprint`, `backupFile`
(the absolute path of the backup), `sha256` (`sha256:` and the hex digest of the backup), `byteLength`,
`createdAt`, and `maxAgeSeconds` (at most 31 days).

`apply` accepts a backup only when all of these hold: the binding names the database, package
digest, and schema fingerprint this database records as active; `createdAt` is no older than
`maxAgeSeconds` and not in the future; `backupFile` is absolute and is a regular file rather than
a symbolic link, owned by the user running `apply`, with mode `0600` and exactly one hard link;
its length and SHA-256 digest equal the binding. Take the backup with your PostgreSQL tooling
immediately before `apply`, `chmod 600` it, and record its digest and length in the binding. A
backup that fails any check refuses the apply before maintenance begins, with
`apply.backup_evidence.refused`, and `plan` with the same `--backup` runs the same checks without
changing anything. The ledger row of the activation records each binding path with the backup's
path, digest, length, and creation time.

### What `test` rehearses

With `--baseline-package`, `test` rebuilds the predecessor schema from the baseline package's
sources on the disposable database, checks that it reproduces the predecessor's
schema fingerprint, then runs the successor migration over it in the order `apply` would:
pre-assertions, the compiler's statements, your reviewed steps, the deferred constraints and
views, and post-assertions. It then requires the result to match the candidate's schema
fingerprint, and rolls everything back before the ordinary journey test runs. A statement
PostgreSQL refuses, an assertion that is not a single boolean column, or a plan that does not reach
the candidate schema fails `test`, and so does a journaled step whose SQL the history journal
refuses. `package`, which requires that receipt, therefore cannot publish a plan `apply` would
reject. The report names the migration, step or assertion, the SQLSTATE and its
class, and the table, column, or constraint PostgreSQL named. It never includes a PostgreSQL
message, because those can quote row values.

The rehearsal runs over empty tables, so it proves that the SQL is valid, ordered, and reaches
the target schema. It does not prove that your data satisfies a step or an assertion: a duplicate
key, a row count outside `affectedRows`, or an assertion that is false over real rows is found
only by your own rehearsal on a restored copy of production and by `apply` itself. Field-encryption
backfill steps are skipped, because they need key material and rows. Reviewed fixture files are
bound by digest only; the rehearsal does not load them.

Package as [deploy a registry](../breg/) describes, with the same `--baseline-package` and
`--reviewed-migrations` arguments. If any source, reviewed artifact, or baseline changes
afterwards, repeat the test and the package.
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 package digest 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_widens();
    crates/registry-breg/src/contract.rs, FieldTypeSource::widens_text_length_of();
    crates/registry-breg/tests/postgres_compiled_schema.rs, text_length_widening_replaces_the_length_check_and_keeps_existing_rows;
    crates/registry-breg/src/tooling.rs, classify_change();
    crates/registry-breg/tests/action_vocabulary_codes.rs;
    crates/registry-bregctl/tests/cli/reviewed_migrations.rs;
    crates/registry-breg/src/postgres/rehearsal.rs;
    crates/registry-breg/src/history_migration.rs, check_reviewed_history_step();
    crates/registry-breg/src/postgres/interlock.rs, execute_reviewed_chunk();
    crates/registry-breg/tests/postgres_migration.rs, reviewed_migration_history();
    crates/registry-breg/src/migration.rs, open_bound_backup(), read_backup_binding(), and check_backup_binding();
    crates/registry-breg/src/migration_plan.rs, ExternalBackupBinding and reviewed_artifact_kind();
    crates/registry-breg/tests/postgres_migration.rs, real_postgres_rehearsal_refuses_a_reviewed_plan_activation_would_refuse;
    products/breg/scripts/test-adopter-workflow.sh. */}

## Activate the successor

For a successor, the runtime file given to `plan` and `apply` still names the active package; the
target comes from `--package`. `apply` activates a package only when it follows the active one:
the package belongs to this registry and its `migrationPlan.fromPackageDigest`, which
`--baseline-package` set, is the active package digest. It refuses a package built against another
baseline, an older package, and a package of another registry with `apply.package.refused`, and a
runtime file whose `identity.databaseId` is not the one the database records with
`apply.database.identity_mismatch`. Run `plan` first: under the apply lock it makes these checks
and the ones `apply` makes before maintenance begins, in the same order, then rolls everything back
and records nothing. It does not run the migration statements; `test` rehearsed those. It lists
the backup bindings the migration requires in `requiredBackups`. A destructive change needs one
binding per environment, passed to both commands by its descriptor path:

```sh
bregctl --format json plan \
  --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-binding.json
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-binding.json
```

`apply` reports `activation: successor` and the `activationId` of the ledger row it recorded;
`bregctl status --runtime-config /etc/breg/runtime.yaml` shows that row with outcome `applied`
beside every earlier one.

:::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.
:::

Update the runtime file's `package.root` to the successor's package directory, 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 refusals
until you restart it onto the successor package. A read checks maintenance status and the
active package when it runs its query. A result it already read under the old package can still be
returned after the activation or after maintenance begins; its audit response entry does not
check the package again.

`apply` refuses the package that is already active under the same roles with
`apply.package.already_active` and exits 1, changing nothing. The exception is a split role
database whose runtime role lost a grant the package gives it: `apply` then reissues the grants.
A deploy job that may meet an environment already on the package runs `plan` first, which exits 0
with `pending: false` and `activation: none` in that case, and calls `apply` only when `pending` is
true.

An activation supersedes every open import authority, because each is bound to the package
it opened under: from the moment the successor is active, no chunk or new run is
admitted under it, and an `import` run in progress stops as `blocked`. The activation closes each
one inside its own transaction and appends one audit record per supersession once it commits. Open a new
authority under the successor to continue a load, as
[move data in bulk](../breg-data/#load-a-governed-entity-through-an-import-window) describes.

{/* Evidence: crates/registry-bregctl/src/lib.rs, ApplyArgs, PlanArgs, and PlanSuccessReport;
    crates/registry-breg/src/migration.rs, apply_verified_package(), MigrationError::AlreadyActive,
    verify_successor_package_binding(), rehearse_begin(), and runtime_grants_missing;
    crates/registry-breg/src/import_authority.rs, supersede_every_open();
    crates/registry-breg/tests/postgres_migration.rs, real_postgres_backfill_and_destructive_recovery_are_bounded_resumable_and_activation_closed;
    crates/registry-breg/src/postgres/interlock.rs;
    crates/registry-breg/src/postgres/context.rs, begin_record_transaction();
    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
writes the transition's response audit entry after the database transaction commits. 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, leaves
the active package untouched, and closes the target's ledger row as `reverted`. The abandoned
package can be applied again once its cause is fixed; that attempt is a separate activation with
an `activationId` of its own, and `bregctl status` lists both rows.

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

## Roll back by rolling forward

Packages apply forward only: `apply` activates a package only when its
`migrationPlan.fromPackageDigest` is the active package digest, so the package that was active
before, or any older one, is refused with `apply.package.refused`. 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. `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, and apply the successor with the active package as `--baseline-package`, as the
   sections above describe, then update the runtime file and restart.

The revert is a new package with its own activation, 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 preserve its [audit files](../breg-retention/#keep-the-audit-journal),
   including operator companion files, in your append-only archive.
2. Restore the database from the backup you took before the `apply`, with your PostgreSQL tooling.
3. Point the runtime file's `package.root` at the package that was active when the backup was
   taken, and run `bregctl verify`. The restored activation ledger ends at that package, so
   `bregctl status` no longer lists the activations committed after the backup.
4. A logical restore is a new database, so the server refuses to serve it until you adopt it:
   run `bregctl instance-claim adopt` as
   [back up and restore the database](#back-up-and-restore-the-database) describes, then start
   the server. The adopt supersedes every import authority the backup held open; open again
   only the ones you still need.

A restore discards every database write and activation 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. Build
the next successor with the restored active package as `--baseline-package`, not the discarded one.

{/* Evidence: crates/registry-breg/src/migration.rs, MigrationError::PackageBinding;
    crates/registry-bregctl/src/lib.rs, apply_lifecycle_failure();
    crates/registry-breg/src/package.rs, CompiledRegistryChangeCode::FieldRemoved. */}

## Back up and restore the database

Take a logical backup of the serving database with the PostgreSQL tools, as an administrator, before
every activation:

```sh
pg_dump --format=custom --file=/srv/backup/registry-before-build-2.dump registry
```

To restore it, stop every `breg` process that serves the registry, then recreate the database with
the database-level statements from [provision PostgreSQL](../breg/#provision-postgresql), which a
dump does not carry, and restore the dump into it as an administrator:

```sql
CREATE DATABASE registry_restored;
REVOKE ALL ON DATABASE registry_restored FROM PUBLIC;
GRANT CONNECT ON DATABASE registry_restored TO registry_migration, registry_runtime;
```

```sh
pg_restore --exit-on-error --dbname=registry_restored /srv/backup/registry-before-build-2.dump
```

For a spatial project, also repeat the `REVOKE CREATE ON DATABASE` statement. Point the runtime
file's database references at the restored database, then check it before anything serves from
it:

```sh
bregctl verify --runtime-config /etc/breg/runtime.yaml
bregctl --format json instance-claim status --runtime-config /etc/breg/runtime.yaml
```

If the restored database is pinned in maintenance, run
[`migration reconcile`](#recover-a-failed-activation) before going further.

Every registry database holds an instance claim: the PostgreSQL system identifier and database
object identifier of the database the registry serves from, recorded at the first apply. A restored
copy carries the claim of the database it was dumped from, so `instance-claim status` reports
`"matches": false`, `bregctl doctor` refuses with `startup.instance_claim.mismatch`, `breg` exits
logging that the database is not the instance its claim names, and `GET /ready` answers `503`. The refusal exists because two databases serving one registry become divergent writers: each
accepts writes, admits imports under the authorities the backup held open, and delivers the same
outbox work from the backup onward, and nothing can merge the two histories afterwards.

Every activation, including the first apply and the apply that adopts a database into the
activation ledger, records the claim of the database it runs in when the database records none,
so a registry upgraded from a release without the claim is claimed by its next `apply` and needs
no adopt. An activation never replaces a claim that is already recorded: a restored copy keeps
the claim of its original until you adopt it.

The claim is checked when `breg` starts and on every `GET /ready`, not on each request. A process
that keeps running while its database host name is repointed at a restored copy keeps serving
requests on the connections it opens until something acts on its readiness `503`. Gate traffic
on `GET /ready`, or restart every `breg` process, whenever the database behind the registry
changes.

The system identifier comes from `pg_control_system()`. PostgreSQL grants it to every role, but a
managed service may withhold it. When the migration role cannot call it, the claim is recorded
without a system identifier; when either side lacks one, the claim compares the database object
identifier alone, and `instance-claim status` shows the system identifier as not readable. The
object identifier alone still tells another database in the same cluster apart, but a copy
restored into a new cluster can receive the same object identifier and is not refused. On such a
service the rule that the original is stopped before a copy serves is the only control, so keep
it, and grant `EXECUTE` on `pg_control_system()` to both roles where the service allows it.

Once the database the claim names is stopped for good, adopt the copy:

```sh
bregctl --format json instance-claim adopt \
  --runtime-config /etc/breg/runtime.yaml \
  --acknowledge-original-retired
```

`adopt` runs under the migration credential. Under the same lock an activation takes, it moves the
claim to this database with its epoch raised by one, supersedes every open
[import authority](../breg-data/#load-a-governed-entity-through-an-import-window), all in one
transaction. Once that commits, it appends each superseded authority's transition record and an
audit entry naming the previous and the adopted claim and the authorities it superseded to the
`bregctl` companion of the configured audit destination. A backup holds the authorities that were open
when it was taken, including any you closed afterwards, so no authority survives an adopt: open
again the ones you still need. On a database the claim already names, as after a physical restore,
it claims the database again the same way and records the event `reclaimed`. Without
`--acknowledge-original-retired` it refuses before it opens a connection. Then run `bregctl doctor`,
start the server, and check `GET /ready`.

:::caution[Adopting does not stop the original]
The claim tells a logical restore from its original, not a physical one. A base backup, a
point-in-time recovery, a volume snapshot, or a promoted replica keeps the original's system
identifier and database object identifier, so nothing refuses it: keeping the original stopped,
or fenced from clients, while such a copy serves is your job. An adopt changes only the
copy's claim, so an original that is still running keeps serving. Pass
`--acknowledge-original-retired` only once no client can reach it.

A physical copy also brings back every import authority that was open at its backup point, including
any you closed afterwards. Before it serves, run `bregctl instance-claim adopt` on it, which
supersedes every open authority, as
[Back up, restore, and drill recovery](../advanced/back-up-and-restore/#after-a-physical-restore)
shows.
:::

An upgrade with `pg_upgrade` moves the registry into a new cluster with a new system identifier,
so it needs the same adopt once the old cluster is stopped. A failover to a streaming replica
keeps the system identifier and needs none.

{/* Evidence: crates/registry-breg/src/instance_claim.rs, record_if_unclaimed(), check(), live_identity(),
    adopt_in();
    crates/registry-breg/src/postgres/interlock.rs, adopt_pre_ledger_database() and activate_verified_package();
    crates/registry-breg/tests/postgres_migration.rs,
    real_postgres_an_activation_records_the_claim_a_database_has_never_recorded and
    real_postgres_an_activation_keeps_a_claim_that_names_another_database;
    crates/registry-breg/src/import_authority.rs, supersede_every_open();
    crates/registry-breg/src/instance_claim.rs, InstanceClaimService::adopt();
    crates/registry-breg/src/startup.rs, verify_instance_claim();
    crates/registry-bregctl/src/lib.rs, InstanceClaimAdoptArgs and instance_claim_failure();
    crates/registry-breg/tests/postgres_startup.rs, a_restored_copy_refuses_to_serve_until_adopted
    and a_database_that_withholds_its_system_identifier_still_serves_and_refuses_a_copy;
    crates/registry-breg/tests/postgres_import_authority.rs,
    adopting_a_restored_copy_supersedes_every_open_authority. */}

## 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 reviewer's view; pass `--runtime-config` for the comparison against the activated package. |
| `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 package digest 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. |
| `test` reports `migration.rehearsal.step_failed`, `migration.rehearsal.assertion_failed`, or `migration.rehearsal.compiler_statement_failed` | PostgreSQL refused the named step, assertion, or generated statement when `test` rehearsed the successor over the predecessor schema, so `apply` would refuse it too. The message names the SQLSTATE, its class, and the object. Correct the reviewed SQL or the project, then repeat test and package. |
| `test` reports `migration.rehearsal.history_step_refused` | The named step changes rows, and `apply` journals every row it changes, but the step's SQL is not an update the journal can record: it does not start with `UPDATE` once leading comments are set aside, it holds a second statement or a semicolon before its end, it names a record metadata column (`record_revision`, `record_lifecycle`, `active_package_revision`, `created_at`, `updated_at`) anywhere, even in a read, it uses a Unicode-escape identifier or string (`U&"..."` or `U&'...'`), a transactional step names `record_id`, it uses a refused statement word (`insert`, `delete`, `truncate`, `alter`, `drop`, `create`, `merge`) outside a comment or plain string literal, or inside a dollar-quoted body or a literal holding a backslash, or its objects span more than the step's entity. Correct the reviewed SQL, then repeat test and package. |
| `test` reports `migration.rehearsal.schema_mismatch` | The rehearsed migration does not reach the candidate schema, for example a required field the reviewed steps never constrain. Activation would refuse it. |
| `test` reports `migration.rehearsal.baseline_unavailable` or `migration.rehearsal.baseline_not_reproducible` | This `bregctl` cannot rebuild the active package's schema from its sources. Run `test` with a `bregctl` release that compiles the predecessor, or report the mismatch. |
| `apply` reports `apply.migration.statement_failed` | PostgreSQL refused a statement after maintenance began. The message names the SQLSTATE, its class, and the table, column, or constraint PostgreSQL reported, for example `23502` for stored rows a new `NOT NULL` refuses or `22P02` for a value a reviewed step cannot convert. Fix that cause and retry the same target, or run `migration reconcile` as for `apply.migration.failed`. |
| `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.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. Keep the active package until the model changes, then build the successor from that change. Nothing was changed. |
| `apply` reports `apply.package.already_active` | The database already runs this package under the configured roles, so there is nothing to activate and nothing was changed. `bregctl status` shows the active package. A deploy job runs `plan` first and applies only when it reports `pending: true`. |
| `apply` or `plan` reports `apply.package.refused` with `does not follow the active package` | The package's `migrationPlan.fromPackageDigest` is not the active package digest, or the package belongs to another registry: it was built against another baseline, it is older than the active package, or it skips a successor this database has not activated yet. Rebuild the successor with `--baseline-package` naming the active package directory. To go back, roll forward or restore, as [roll back by rolling forward](#roll-back-by-rolling-forward) describes. |
| `apply` or `plan` reports `apply.database.identity_mismatch` | The database records another database id than the runtime file's `identity.databaseId`. Point `database.migrationUrlRef` at the database the file names, or correct `identity.databaseId`. Nothing was changed. |
| `apply` or `plan` reports `apply.backup_evidence.refused` | A backup binding is missing, names another database, package, or schema fingerprint than the active one, is older than `maxAgeSeconds`, or its `backupFile` fails the file, owner, mode, length, or digest check. Take a fresh backup of this environment's database, write its binding, and pass it with `--backup`. Nothing was changed. |
| `apply` reports `apply.package.active_mismatch` | The runtime file's `package.root` names a package the database does not record as active and ready. Run `bregctl status` to read the active digest, set `package.root` to that package directory, 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 dedicated history-erasure coverage table 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. |
| `breg` refuses to start, or `doctor` reports `startup.instance_claim.mismatch` | The database is not the one the registry's instance claim names, as after a logical restore or a `pg_upgrade`. Stop the database the claim names for good, then run `instance-claim adopt` as [back up and restore the database](#back-up-and-restore-the-database) describes. |
| `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, 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.