Skip to content
Registry StackDocsv0.38.0

Build a production candidate

For the data publisher

View as Markdown

If you finished Extend a registry with a module, you have bregctl installed and a tutorial-work directory. This tutorial builds from a fresh project of its own, tutorial-work/candidate-project, because the module tutorial edits its project and every edit changes the digests. In this tutorial you read what the production profile finds in the new project, run its journeys against a PostgreSQL you start yourself, and turn the result into a package that bregctl verify checks the way the runtime checks its files at startup. You then promote that one package through two local databases, staging and production, the way a release moves between environments: you plan and apply it in each, and confirm that both serve the same registry revision. Deploy a registry takes the same steps to a PostgreSQL you run for real.

Outcome
A package built from your project, accepted by bregctl verify, and activated in a staging and a production database that serve the same registry revision.
Time
About 45 minutes
Level
Production build with synthetic data and a local issuer key
Prerequisites
The bregctl and breg pair from one releaseDockerOpenSSL 3curl and jq

This tutorial runs both bregctl and breg. The installer from Create and query your first registry installs the two together from one release; Obtain the runtime shows how to pin that release.

Open a terminal in the directory that holds tutorial-work, or in any directory you want to work in. Create the project this tutorial builds from:

Terminal window
mkdir -p tutorial-work
bregctl init tutorial-work/candidate-project

The project names no environment, instance, or database. The runtime configuration you write later names all three, so the package you build here is the same package whichever deployment reads it.

Confirm the tools. The token steps need an OpenSSL that signs with Ed25519; the LibreSSL that macOS ships as /usr/bin/openssl does not, so install OpenSSL 3 and put it first on PATH.

Terminal window
bregctl --version
docker --version
openssl version

Set an owner-only file mode for this shell, so every key, password, and token the commands create is readable by you alone:

Terminal window
umask 077

Every other file this tutorial creates sits under tutorial-work/candidate. The digests shown on this page come from the unchanged bregctl init project; if you edit anything in candidate-project, yours differ, and that is expected.

check --production compiles the project the way test and package do. It refuses what the production profile forbids and reports findings, which are decisions it wants a person to review rather than errors:

Terminal window
bregctl check --production tutorial-work/candidate-project
Production check passed.
revision sha256:a44d4d1504891639ac3f1ae217ca7df8f50f87dc78e10bd92cdf084a123deccc
finding access.profile.unrestricted_collection entities[id=record].accessProfiles[id=operator].rowBoundaries
this profile can list all rows, subject only to query bounds; caller filters
are not authorization. Add a claim-bound row restriction or review this
registry-wide access
finding access.profile.unrestricted_rows entities[id=record].accessProfiles[id=evidence-source].rowBoundaries
this profile has no claim-bound row restriction for its granted operations;
requestVisibility owner limits request reads only, and other lifecycle rules
still apply. Review this registry-wide access
0 errors, 2 findings.

The generated project ships both findings on purpose: the operator profile lists every record because one operations team runs the whole registry, and the evidence-source profile looks a record up by its code for any record in the registry. The comment that introduces each profile in registry.yaml says how to close it (a rowBoundaries entry, or removing list from the operator’s grant). Closing them would also change the journeys, so treat them as reviewed and see what a stricter gate does with them:

Terminal window
bregctl check --production --deny-findings tutorial-work/candidate-project
bregctl check refused.
finding access.profile.unrestricted_collection entities[id=record].accessProfiles[id=operator].rowBoundaries
this profile can list all rows, subject only to query bounds; caller filters
are not authorization. Add a claim-bound row restriction or review this
registry-wide access
finding access.profile.unrestricted_rows entities[id=record].accessProfiles[id=evidence-source].rowBoundaries
this profile has no claim-bound row restriction for its granted operations;
requestVisibility owner limits request reads only, and other lifecycle rules
still apply. Review this registry-wide access
refused on 2 findings.

The command exits with status 1. That is the gate for a pipeline that must never build a candidate with an unreviewed finding; the rest of this tutorial runs without it.

One Ed25519 key stands in for your identity provider: it signs the access tokens the journeys send, and its public half is the static JWKS the runtime reads.

Terminal window
mkdir -p tutorial-work/candidate/secrets tutorial-work/candidate/postgres
openssl genpkey -algorithm ED25519 -out tutorial-work/candidate/issuer.pem
issuer_x=$(openssl pkey -in tutorial-work/candidate/issuer.pem -pubout -outform DER | tail -c 32 | openssl base64 -A | tr '+/' '-_' | tr -d '=')
printf '{"keys":[{"kty":"OKP","crv":"Ed25519","alg":"EdDSA","kid":"candidate-issuer","x":"%s"}]}' "$issuer_x" > tutorial-work/candidate/secrets/issuer-jwks

The runtime connects to PostgreSQL over TLS only, and the schema test uses the same connection code, so the container needs a server certificate. Create a small certificate authority and a certificate for localhost, then start a PostgreSQL 17 container on port 5433 so it stays clear of a PostgreSQL you may already run:

Terminal window
openssl req -x509 -new -nodes -newkey rsa:2048 -sha256 -days 2 -subj '/CN=Candidate tutorial CA' -keyout tutorial-work/candidate/postgres/ca.key -out tutorial-work/candidate/postgres/ca.pem
openssl req -new -nodes -newkey rsa:2048 -subj '/CN=localhost' -keyout tutorial-work/candidate/postgres/server.key -out tutorial-work/candidate/postgres/server.csr
printf 'subjectAltName=DNS:localhost\n' > tutorial-work/candidate/postgres/server.ext
openssl x509 -req -sha256 -days 2 -in tutorial-work/candidate/postgres/server.csr -CA tutorial-work/candidate/postgres/ca.pem -CAkey tutorial-work/candidate/postgres/ca.key -CAcreateserial -extfile tutorial-work/candidate/postgres/server.ext -out tutorial-work/candidate/postgres/server.crt
printf 'POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 24)" > tutorial-work/candidate/postgres/postgres.env
docker run --detach --name breg-candidate-postgres --env-file tutorial-work/candidate/postgres/postgres.env --publish 127.0.0.1:5433:5432 postgres:17.11@sha256:67f41722b7a8cbdb868a44a4995c846eddfdc2973bccb291ce937dce88ad5675
until docker exec breg-candidate-postgres pg_isready -h localhost -U postgres >/dev/null; do sleep 1; done
docker cp tutorial-work/candidate/postgres/server.crt breg-candidate-postgres:/var/lib/postgresql/data/server.crt
docker cp tutorial-work/candidate/postgres/server.key breg-candidate-postgres:/var/lib/postgresql/data/server.key
docker exec breg-candidate-postgres sh -c 'chown postgres:postgres /var/lib/postgresql/data/server.* && chmod 600 /var/lib/postgresql/data/server.key'
docker exec breg-candidate-postgres psql -U postgres -c 'ALTER SYSTEM SET ssl = on' -c 'SELECT pg_reload_conf()'

The last command prints ALTER SYSTEM and a one-row pg_reload_conf result of t.

bregctl test needs two ordinary roles and one database it may fill. The migration role owns the five managed schemas and installs the compiled schema; the runtime role runs the journeys and must own nothing. Neither may be a superuser, create databases or roles, or bypass row-level security, which is what the CREATE ROLE options spell out. The test refuses a database whose managed schemas already hold objects, so the database serves one run: drop and recreate it before every rerun.

Terminal window
mp=$(openssl rand -hex 16)
rp=$(openssl rand -hex 16)
docker exec -i breg-candidate-postgres psql -v ON_ERROR_STOP=1 -U postgres <<SQL
CREATE ROLE registry_migration LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS PASSWORD '$mp';
CREATE ROLE registry_runtime LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS PASSWORD '$rp';
SQL
printf 'postgresql://registry_migration:%s@localhost:5433/registry_candidate_test' "$mp" > tutorial-work/candidate/secrets/migration-database-url
printf 'postgresql://registry_runtime:%s@localhost:5433/registry_candidate_test' "$rp" > tutorial-work/candidate/secrets/runtime-database-url
openssl rand -hex 32 > tutorial-work/candidate/secrets/audit-key
openssl rand -hex 32 > tutorial-work/candidate/secrets/cursor-key

The secrets directory now holds the two connection URLs, the audit hash key, the cursor key, and the JWKS, one owner-only file per secret, which is what the runtime’s file secret provider requires: it refuses a secret that is not a plain file you own with mode 0400 or 0600. Now the database, in the block you rerun after every test. The block drops the database before creating it, so a rerun starts from nothing and everything the previous run wrote is gone:

Terminal window
docker exec breg-candidate-postgres psql -v ON_ERROR_STOP=1 -U postgres -c 'DROP DATABASE IF EXISTS registry_candidate_test' -c 'CREATE DATABASE registry_candidate_test'
docker exec -i breg-candidate-postgres psql -v ON_ERROR_STOP=1 -U postgres -d registry_candidate_test <<'SQL'
CREATE EXTENSION IF NOT EXISTS btree_gist;
REVOKE ALL ON DATABASE registry_candidate_test FROM PUBLIC;
GRANT CONNECT ON DATABASE registry_candidate_test TO registry_migration, registry_runtime;
CREATE SCHEMA registry_internal AUTHORIZATION registry_migration;
CREATE SCHEMA registry_data AUTHORIZATION registry_migration;
CREATE SCHEMA registry_source AUTHORIZATION registry_migration;
CREATE SCHEMA registry_derived AUTHORIZATION registry_migration;
CREATE SCHEMA registry_context AUTHORIZATION registry_migration;
REVOKE ALL ON SCHEMA registry_internal, registry_data, registry_source, registry_derived, registry_context FROM PUBLIC;
SQL

bregctl test reads the runtime configuration document the runtime itself reads at startup. identity names the environment, instance, and database this deployment is, and its environment and databaseInitializationEnvironment must be the same value. package.root names an empty directory because no package exists yet, listener.bind is never opened because the journeys are driven in-process, and every path must be absolute, which the unquoted heredoc arranges by expanding $PWD:

Terminal window
mkdir -p tutorial-work/candidate/empty-package
cat > tutorial-work/candidate/runtime-test.yaml <<EOF
apiVersion: registry.registrystack.org/breg-runtime/v1alpha1
kind: BRegRuntimeConfig
listener:
bind: 127.0.0.1:8080
identity:
environment: development
instanceId: generic-registry-1
databaseId: generic-registry-db-1
databaseInitializationEnvironment: development
secretProviders:
file:
root: $PWD/tutorial-work/candidate/secrets
database:
runtimeUrlRef: secret:file/runtime-database-url
migrationUrlRef: secret:file/migration-database-url
pool:
maxSize: 4
roles:
migration: registry_migration
runtime: registry_runtime
package:
root: $PWD/tutorial-work/candidate/empty-package
authentication:
oidc:
issuer: https://issuer.example.invalid
audience: generic-registry
allowedAlgorithm: EdDSA
accessTokenType: at+jwt
scopeClaim: scope
scopeSeparator: " "
allowedClients:
- generic-registry-client
maxTokenLifetimeSeconds: 3600
leewayMilliseconds: 30000
jwksSource:
kind: static
documentRef: secret:file/issuer-jwks
authorityClaims:
principal: registry_principal
purpose: registry_purpose
audit:
hashKeyRef: secret:file/audit-key
destination: file
path: $PWD/tutorial-work/candidate/audit/breg.jsonl
cursor:
secretRef: secret:file/cursor-key
EOF

The journeys authenticate with bearer tokens. This script issues one, signed by the issuer key, that carries the claims a step declares: the principal, the actor kind, the scope, the purpose, and for the reader the registry_record_status claim its row boundary compares. Base Registry Engine (BReg) refuses a token whose registry_actor_kind claim is missing or is not exactly human, agent, or service; these tokens authenticate automated journeys, so the script marks both of them service. Then it issues the operator’s and the reader’s tokens:

cat > tutorial-work/candidate/issue-token.sh <<'EOF'
#!/usr/bin/env bash
# issue-token.sh <principal> <scope> <purpose> <output-file> [<extra-claims-json>]
set -euo pipefail
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
now=$(date +%s)
header=$(printf '{"alg":"EdDSA","kid":"candidate-issuer","typ":"at+jwt"}' | b64url)
claims=$(printf '{"iss":"https://issuer.example.invalid","aud":"generic-registry","client_id":"generic-registry-client","sub":"%s","registry_principal":"%s","registry_actor_kind":"service","scope":"%s","registry_purpose":"%s","iat":%s,"exp":%s%s}' \
"$1" "$1" "$2" "$3" "$now" "$((now + 3600))" "${5:+,$5}" | b64url)
signing_input=$(mktemp)
printf '%s.%s' "$header" "$claims" > "$signing_input"
signature=$(openssl pkeyutl -sign -rawin -inkey "$(dirname "$0")/issuer.pem" -in "$signing_input" | b64url)
rm -f "$signing_input"
umask 077
printf '%s.%s.%s' "$header" "$claims" "$signature" > "$4"
EOF
chmod 700 tutorial-work/candidate/issue-token.sh
tutorial-work/candidate/issue-token.sh generic-registry-operator registry:generic:operate registry-operations tutorial-work/candidate/secrets/operator-token
tutorial-work/candidate/issue-token.sh generic-registry-reader registry:generic:read registry-reporting tutorial-work/candidate/secrets/reader-token '"registry_record_status":"active"'

The credentials file binds one token to every journey step. A token whose claims differ from the step’s declared claims fails the run; the test compares them exactly rather than trusting the label on the binding.

Terminal window
cat > tutorial-work/candidate/credentials.yaml <<'EOF'
apiVersion: registry.registrystack.org/breg-schema-test-credentials/v1
kind: SchemaTestCredentials
bindings:
- journeyId: record-lifecycle
stepId: create-record-group
credential:
type: bearer
tokenRef: secret:file/operator-token
- journeyId: record-lifecycle
stepId: create-record
credential:
type: bearer
tokenRef: secret:file/operator-token
- journeyId: record-lifecycle
stepId: get-record
credential:
type: bearer
tokenRef: secret:file/operator-token
- journeyId: record-lifecycle
stepId: read-record-within-the-claim
credential:
type: bearer
tokenRef: secret:file/reader-token
- journeyId: record-lifecycle
stepId: retire-record
credential:
type: bearer
tokenRef: secret:file/operator-token
- journeyId: record-lifecycle
stepId: read-record-outside-the-claim
credential:
type: bearer
tokenRef: secret:file/reader-token
- journeyId: record-lifecycle
stepId: list-records
credential:
type: bearer
tokenRef: secret:file/operator-token
EOF
Terminal window
SSL_CERT_FILE="$PWD/tutorial-work/candidate/postgres/ca.pem" bregctl test tutorial-work/candidate-project \
--runtime-config "$PWD/tutorial-work/candidate/runtime-test.yaml" \
--credentials "$PWD/tutorial-work/candidate/credentials.yaml" \
--output "$PWD/tutorial-work/candidate/test-receipt.json"
Fixture run passed. 1 journey.
profile production
registry revision sha256:a44d4d1504891639ac3f1ae217ca7df8f50f87dc78e10bd92cdf084a123deccc
schema fingerprint sha256:c479ff47b061e3c53c6f460e7a897a58e867f7c090c8fd852d8075ab93d9e8e5
successful journeys record-lifecycle
receipt sha256 2345f4e51538a3bc1acb80ebd6f015d77eac1d084e57f706a6a8510005d2435e
receipt bytes 746

SSL_CERT_FILE points the TLS client at your certificate authority, and the URL host localhost must match the certificate’s name. The receipt records the registry revision, the schema fingerprint the run measured, digests of the project sources, migration plan, and journey file, and the journey that passed. It names no environment, instance, or database. package requires this receipt.

Build into a new directory. package publishes into it once; after that it refuses the directory with package.output.refused, so the next candidate needs a build-2 of its own.

Terminal window
bregctl package tutorial-work/candidate-project \
--test-receipt "$PWD/tutorial-work/candidate/test-receipt.json" \
--output "$PWD/tutorial-work/candidate/build-1"
Sealed and published a deployment package.
profile production
package digest sha256:f06c407cbddba19a094664d880e69a8935a0d8c7ad5f73d41f15760ef10463f7
registry revision sha256:a44d4d1504891639ac3f1ae217ca7df8f50f87dc78e10bd92cdf084a123deccc
package files 19

The package is in tutorial-work/candidate/build-1/package, beside a copy of the receipt: the effective model, the DDL and migration plan, the OpenAPI document and JSON Schemas, the inventories, the manifest projection, the journeys, and the project source it was built from. SHA256SUMS lists the SHA-256 of every file, and the package digest is the SHA-256 of SHA256SUMS, so changing any file changes the digest. The runtime configuration names the package next.

Copy runtime-test.yaml to runtime.yaml:

Terminal window
cp tutorial-work/candidate/runtime-test.yaml tutorial-work/candidate/runtime.yaml

Change one value in the copy: package.root becomes the published package directory.

package:
root: <working-directory>/tutorial-work/candidate/build-1/package
Terminal window
bregctl verify --runtime-config "$PWD/tutorial-work/candidate/runtime.yaml"
Verified the package against the runtime it is bound to.
assurance runtime_bound
package digest sha256:f06c407cbddba19a094664d880e69a8935a0d8c7ad5f73d41f15760ef10463f7
registry id generic-registry
registry version 0.1.0
registry revision sha256:a44d4d1504891639ac3f1ae217ca7df8f50f87dc78e10bd92cdf084a123deccc
modules 1
entities 2
routes 8
access entries 8
queries 4
event deliveries 0
DDL statements 25
generated artifacts 14

runtime_bound means the check read the runtime configuration the runtime reads at startup, recomputed every file digest against SHA256SUMS, and re-derived every generated artifact from the package’s own source. It opens no database: reading the database is what plan does, in the next section.

To pin the exact package, add package.expectedDigest with the digest package printed. verify and the runtime then refuse any other package at package.root, naming both digests. Once a database has activated a package, the runtime also refuses to serve any package its activation ledger does not name as active, pin or no pin.

Promote the package to staging and production

Section titled “Promote the package to staging and production”

You built and tested the package once. Each environment now plans and applies that same package directory against its own database; nothing is rebuilt between them.

In the same shell, so umask 077 still applies, create registry_staging and registry_production the way you created the test database, and write each one’s connection URLs beside the others. The roles and their passwords are the ones you already made:

Terminal window
for env in staging production; do
docker exec breg-candidate-postgres psql -v ON_ERROR_STOP=1 -U postgres -c "CREATE DATABASE registry_$env"
docker exec -i breg-candidate-postgres psql -v ON_ERROR_STOP=1 -U postgres -d "registry_$env" <<SQL
CREATE EXTENSION IF NOT EXISTS btree_gist;
REVOKE ALL ON DATABASE registry_$env FROM PUBLIC;
GRANT CONNECT ON DATABASE registry_$env TO registry_migration, registry_runtime;
CREATE SCHEMA registry_internal AUTHORIZATION registry_migration;
CREATE SCHEMA registry_data AUTHORIZATION registry_migration;
CREATE SCHEMA registry_source AUTHORIZATION registry_migration;
CREATE SCHEMA registry_derived AUTHORIZATION registry_migration;
CREATE SCHEMA registry_context AUTHORIZATION registry_migration;
REVOKE ALL ON SCHEMA registry_internal, registry_data, registry_source, registry_derived, registry_context FROM PUBLIC;
SQL
sed "s#/registry_candidate_test\$#/registry_$env#" tutorial-work/candidate/secrets/migration-database-url > "tutorial-work/candidate/secrets/$env-migration-url"
sed "s#/registry_candidate_test\$#/registry_$env#" tutorial-work/candidate/secrets/runtime-database-url > "tutorial-work/candidate/secrets/$env-runtime-url"
done

Derive staging.yaml and production.yaml from runtime.yaml. Both keep package.root on build-1/package; each gets its own identity, connection URLs, listener port, and audit file:

Terminal window
port=8081
for env in staging production; do
mkdir -p "tutorial-work/candidate/audit-$env"
sed -e "s#bind: 127.0.0.1:8080#bind: 127.0.0.1:$port#" \
-e "s#: development\$#: $env#" \
-e "s#instanceId: generic-registry-1#instanceId: generic-registry-$env#" \
-e "s#databaseId: generic-registry-db-1#databaseId: generic-registry-$env-db#" \
-e "s#secret:file/runtime-database-url#secret:file/$env-runtime-url#" \
-e "s#secret:file/migration-database-url#secret:file/$env-migration-url#" \
-e "s#/audit/breg.jsonl#/audit-$env/breg.jsonl#" \
tutorial-work/candidate/runtime.yaml > "tutorial-work/candidate/$env.yaml"
port=$((port + 1))
done
diff tutorial-work/candidate/staging.yaml tutorial-work/candidate/production.yaml

diff lists the listener, the four identity values, the two URL references, and the audit path, and nothing else. The first apply records databaseId in its database, and every later plan, apply, and startup refuses a runtime file that names another.

plan makes the checks apply makes, under the same lock, and rolls them back, so it changes nothing. Run it for both environments first:

Terminal window
export SSL_CERT_FILE="$PWD/tutorial-work/candidate/postgres/ca.pem"
for env in staging production; do
bregctl plan --runtime-config "$PWD/tutorial-work/candidate/$env.yaml" \
--package "$PWD/tutorial-work/candidate/build-1/package"
done

Each plan reports activation initial and the package digest package printed. Apply the package, then read what each database recorded:

Terminal window
for env in staging production; do
bregctl apply --initial --runtime-config "$PWD/tutorial-work/candidate/$env.yaml" \
--package "$PWD/tutorial-work/candidate/build-1/package"
bregctl status --runtime-config "$PWD/tutorial-work/candidate/$env.yaml"
done

--initial activates the first package in a database that has never activated one; every later apply omits it. status reads the activation ledger: both databases report the same active package digest, maintenance status ready, and one ledger entry with plan kind initial and outcome applied. Their activation ids differ, because each database records its own activation.

Start one runtime per environment, wait until both are ready, and ask each for its registry metadata with the operator token. The header file keeps the token off the command line:

Terminal window
breg --runtime-config "$PWD/tutorial-work/candidate/staging.yaml" > tutorial-work/candidate/staging.log 2>&1 &
staging_pid=$!
breg --runtime-config "$PWD/tutorial-work/candidate/production.yaml" > tutorial-work/candidate/production.log 2>&1 &
production_pid=$!
until curl -fsS http://127.0.0.1:8081/ready >/dev/null && curl -fsS http://127.0.0.1:8082/ready >/dev/null; do sleep 1; done
printf 'Authorization: Bearer %s\n' "$(cat tutorial-work/candidate/secrets/operator-token)" > tutorial-work/candidate/secrets/operator-header
for port in 8081 8082; do
curl -fsS -H @tutorial-work/candidate/secrets/operator-header "http://127.0.0.1:$port/v1/registry?accessProfile=operator" | jq -r .revision
done
kill "$staging_pid" "$production_pid"

Both lines print the registry revision package printed. Staging and production serve the same registry because they run the same package, which is the point of promoting the artifact rather than rebuilding it.

The ledger decides what each database accepts. Apply the active package to staging again, then point a copy of the staging file at the production database and plan against it:

Terminal window
bregctl apply --runtime-config "$PWD/tutorial-work/candidate/staging.yaml" \
--package "$PWD/tutorial-work/candidate/build-1/package"
sed 's#secret:file/staging-#secret:file/production-#' tutorial-work/candidate/staging.yaml > tutorial-work/candidate/staging-on-production.yaml
bregctl plan --runtime-config "$PWD/tutorial-work/candidate/staging-on-production.yaml" \
--package "$PWD/tutorial-work/candidate/build-1/package"

Both exit with status 1 and change nothing. The first reports apply.package.already_active: the package is already active, so a deploy job runs plan and applies only when it reports pending true. The second reports apply.database.identity_mismatch: the production database recorded generic-registry-production-db, and the file names generic-registry-staging-db. Two more refusals hold once you promote a successor: an older package, or one that does not name the active package as its predecessor, is refused with apply.package.refused, and a backup binding recorded for another database is refused with apply.backup_evidence.refused.

This project has no successor yet. When a later build changes the schema, it is packaged once and promoted the same way, and a destructive change requires one backup per environment. Each environment takes its own backup and writes its own binding, which names its databaseId, so the production apply never accepts the staging backup. For a successor whose reviewed migration names modules/core/migrations/retire-legacy-field/backup.json, the per-environment step is:

Terminal window
bregctl plan --runtime-config "$PWD/tutorial-work/candidate/staging.yaml" \
--package "$PWD/tutorial-work/candidate/build-2/package" \
--backup "modules/core/migrations/retire-legacy-field/backup.json=$PWD/tutorial-work/candidate/backups/staging-binding.json"
bregctl apply --runtime-config "$PWD/tutorial-work/candidate/staging.yaml" \
--package "$PWD/tutorial-work/candidate/build-2/package" \
--backup "modules/core/migrations/retire-legacy-field/backup.json=$PWD/tutorial-work/candidate/backups/staging-binding.json"

Production repeats it with production.yaml and its own binding. apply reports activation successor, and status then lists two ledger entries, initial and successor, the second naming the first package as its predecessor. Change a live registry covers writing the reviewed migration and the binding.

Terminal window
docker rm -f breg-candidate-postgres
rm -f tutorial-work/candidate/issuer.pem tutorial-work/candidate/postgres/postgres.env
rm -rf tutorial-work/candidate/secrets

A project reviewed under the production profile; a schema-test receipt from its journeys, run against PostgreSQL 17 over TLS with tokens from a static JWKS; and a package whose digest covers every file it holds, which bregctl verify accepts with runtime_bound assurance under the runtime configuration you wrote. You then promoted that one package to a staging and a production database: plan checked each before apply changed it, status read what each activation ledger recorded, and both runtimes served the same registry revision. The databases were disposable, and the successor step is the one you repeat for every later build.

SymptomCause and next move
bregctl test reports test.database.unavailableThe database already holds managed objects from an earlier run, or the TLS connection failed; the message is the same for both. Rerun the database block, and confirm that SSL_CERT_FILE names ca.pem and that the URLs use localhost, the name on the certificate.
bregctl test reports test.output.refusedA receipt already exists at --output. The command never overwrites one, so delete test-receipt.json or name a new file before the rerun.
bregctl test reports test.step.failed and the fixture authority reference was refusedThe token bound to that step does not carry the claims the step declares, or it has expired. The message names the step by index, journeys[0].steps[3]. Check the binding in credentials.yaml, and reissue both tokens with issue-token.sh if the run started more than an hour after you issued them.
bregctl package reports package.output.refused--output already holds a build. package never publishes into a used directory, so name a new one, such as build-2.
bregctl verify reports verify.runtime_config.environment_identity_conflictidentity.environment and identity.databaseInitializationEnvironment in runtime.yaml differ. Set both to the same environment.
verify.package.integrity_refused and does not match its SHA256SUMSA file in the package changed after package published it; the message names each changed file. Rebuild with bregctl package into a new directory and deploy the whole directory.
verify.package.integrity_refused and package.expectedDigest isThe pin names a different package than the one at package.root. Deploy the pinned package or update package.expectedDigest.
The readiness loop never endsA runtime refused to start. Read staging.log or production.log for the refusal code, stop the other runtime with kill, and rerun the block once the cause is fixed.
breg logs startup.package.not_activepackage.root in that runtime file names a package the database has not activated. Point it at build-1/package, or plan and apply the package it names.
curl fails with 401The operator token expired an hour after you issued it. Reissue it with issue-token.sh, rewrite operator-header, and query again.
openssl rejects -rawin or -algorithm ED25519The openssl on PATH is LibreSSL. Install OpenSSL 3 and put it first on PATH.