Versioned archive. You are viewing v0.38.0. For the latest released guidance, use Latest release. Report archive issues on GitHub.
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.
Before you start
Section titled “Before you start”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:
mkdir -p tutorial-workbregctl init tutorial-work/candidate-projectThe 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.
bregctl --versiondocker --versionopenssl versionSet an owner-only file mode for this shell, so every key, password, and token the commands create is readable by you alone:
umask 077Every 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.
Read the production findings
Section titled “Read the production findings”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:
bregctl check --production tutorial-work/candidate-projectProduction 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:
bregctl check --production --deny-findings tutorial-work/candidate-projectbregctl 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.
Create the issuer key
Section titled “Create the issuer key”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.
mkdir -p tutorial-work/candidate/secrets tutorial-work/candidate/postgresopenssl genpkey -algorithm ED25519 -out tutorial-work/candidate/issuer.pemissuer_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-jwksAnyone holding the issuer’s private key can mint tokens the runtime accepts. Never copy it into the project, a package, or a repository. It exists for this exercise and is deleted in the cleanup step.
Start PostgreSQL
Section titled “Start PostgreSQL”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:
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.pemopenssl req -new -nodes -newkey rsa:2048 -subj '/CN=localhost' -keyout tutorial-work/candidate/postgres/server.key -out tutorial-work/candidate/postgres/server.csrprintf 'subjectAltName=DNS:localhost\n' > tutorial-work/candidate/postgres/server.extopenssl 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.crtprintf 'POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 24)" > tutorial-work/candidate/postgres/postgres.envdocker 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:67f41722b7a8cbdb868a44a4995c846eddfdc2973bccb291ce937dce88ad5675until docker exec breg-candidate-postgres pg_isready -h localhost -U postgres >/dev/null; do sleep 1; donedocker cp tutorial-work/candidate/postgres/server.crt breg-candidate-postgres:/var/lib/postgresql/data/server.crtdocker cp tutorial-work/candidate/postgres/server.key breg-candidate-postgres:/var/lib/postgresql/data/server.keydocker 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.
Prepare the disposable database
Section titled “Prepare the disposable database”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.
mp=$(openssl rand -hex 16)rp=$(openssl rand -hex 16)docker exec -i breg-candidate-postgres psql -v ON_ERROR_STOP=1 -U postgres <<SQLCREATE ROLE registry_migration LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS PASSWORD '$mp';CREATE ROLE registry_runtime LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS PASSWORD '$rp';SQLprintf 'postgresql://registry_migration:%s@localhost:5433/registry_candidate_test' "$mp" > tutorial-work/candidate/secrets/migration-database-urlprintf 'postgresql://registry_runtime:%s@localhost:5433/registry_candidate_test' "$rp" > tutorial-work/candidate/secrets/runtime-database-urlopenssl rand -hex 32 > tutorial-work/candidate/secrets/audit-keyopenssl rand -hex 32 > tutorial-work/candidate/secrets/cursor-keyThe 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:
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;SQLBind the runtime and the credentials
Section titled “Bind the runtime and the credentials”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:
mkdir -p tutorial-work/candidate/empty-packagecat > tutorial-work/candidate/runtime-test.yaml <<EOFapiVersion: registry.registrystack.org/breg-runtime/v1alpha1kind: BRegRuntimeConfiglistener: bind: 127.0.0.1:8080identity: environment: development instanceId: generic-registry-1 databaseId: generic-registry-db-1 databaseInitializationEnvironment: developmentsecretProviders: file: root: $PWD/tutorial-work/candidate/secretsdatabase: runtimeUrlRef: secret:file/runtime-database-url migrationUrlRef: secret:file/migration-database-url pool: maxSize: 4 roles: migration: registry_migration runtime: registry_runtimepackage: root: $PWD/tutorial-work/candidate/empty-packageauthentication: 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_purposeaudit: hashKeyRef: secret:file/audit-key destination: file path: $PWD/tutorial-work/candidate/audit/breg.jsonlcursor: secretRef: secret:file/cursor-keyEOFThe 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 pipefailb64url() { 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 077printf '%s.%s.%s' "$header" "$claims" "$signature" > "$4"EOFchmod 700 tutorial-work/candidate/issue-token.shtutorial-work/candidate/issue-token.sh generic-registry-operator registry:generic:operate registry-operations tutorial-work/candidate/secrets/operator-tokentutorial-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.
cat > tutorial-work/candidate/credentials.yaml <<'EOF'apiVersion: registry.registrystack.org/breg-schema-test-credentials/v1kind: SchemaTestCredentialsbindings: - 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-tokenEOFRun the journeys
Section titled “Run the journeys”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 746SSL_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.
Package
Section titled “Package”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.
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 19The 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.
Verify the package
Section titled “Verify the package”Copy runtime-test.yaml to runtime.yaml:
cp tutorial-work/candidate/runtime-test.yaml tutorial-work/candidate/runtime.yamlChange one value in the copy: package.root becomes the published package directory.
package: root: <working-directory>/tutorial-work/candidate/build-1/packagebregctl 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 14runtime_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.
Create one database per environment
Section titled “Create one database per environment”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:
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" <<SQLCREATE 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"doneWrite one runtime file per environment
Section titled “Write one runtime file per environment”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:
port=8081for 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))donediff tutorial-work/candidate/staging.yaml tutorial-work/candidate/production.yamldiff 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 and apply in each environment
Section titled “Plan and apply in each environment”plan makes the checks apply makes, under the same lock, and rolls them back, so it changes
nothing. Run it for both environments first:
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"doneEach plan reports activation initial and the package digest package printed. Apply the
package, then read what each database recorded:
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.
Serve both and compare
Section titled “Serve both and compare”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:
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; doneprintf 'Authorization: Bearer %s\n' "$(cat tutorial-work/candidate/secrets/operator-token)" > tutorial-work/candidate/secrets/operator-headerfor 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 .revisiondonekill "$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.
See what apply refuses
Section titled “See what apply refuses”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:
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.yamlbregctl 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.
Promote a reviewed successor
Section titled “Promote a reviewed successor”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:
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.
Clean up
Section titled “Clean up”docker rm -f breg-candidate-postgresrm -f tutorial-work/candidate/issuer.pem tutorial-work/candidate/postgres/postgres.envrm -rf tutorial-work/candidate/secretsdocker rm -f deletes the container and the test, staging, and production databases inside it,
which is what disposable databases are for. Keep build-1 and runtime.yaml if you continue with
Deploy a registry.
What you built
Section titled “What you built”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.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause and next move |
|---|---|
bregctl test reports test.database.unavailable | The 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.refused | A 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 refused | The 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_conflict | identity.environment and identity.databaseInitializationEnvironment in runtime.yaml differ. Set both to the same environment. |
verify.package.integrity_refused and does not match its SHA256SUMS | A 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 is | The pin names a different package than the one at package.root. Deploy the pinned package or update package.expectedDigest. |
| The readiness loop never ends | A 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_active | package.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 401 | The 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 ED25519 | The openssl on PATH is LibreSSL. Install OpenSSL 3 and put it first on PATH. |
- Deploy a registry: provision PostgreSQL for real, write each environment’s runtime configuration, check and activate the package with
plan,apply, andstatus, and serve it withbreg. - Change a live registry when the next build changes the schema.
- Test with journeys to extend the journeys before the next candidate.
- Control access per profile to close the finding with a row boundary.