Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.26.1.
Review changes before updating a registry
For the data publisher
If you are evaluating Base Registry Engine for data that needs approval, this tutorial takes you through an asset-location correction: propose a different site, collect two approvals, and apply the change. A change request is a record that proposes a write to another record and carries it out only after the review its configuration requires, and a review stage is one named round of that review with its own approval count and its own deciding profiles, both declared in declare change requests and actions. You will run the supplied workflow against PostgreSQL, adapt its configuration, and check a refusal.
Before you start
Section titled “Before you start”Create and query your first registry installs breg, bregctl, and mint and
clones the repository at the tag that matches them.
Open a terminal at the root of that breg-tutorial checkout and confirm the binaries answer:
bregctl --versionThis tutorial adds two requirements to that setup.
The example runner calls psql directly, so confirm the PostgreSQL client tools are installed:
psql --versionIt also needs a disposable PostgreSQL database, which you borrow from the quickstart launcher in step 3 rather than from any database you already run.
Run commands from the root of that clone unless a step says otherwise.
The workflow runs through bregctl test, which sends requests through the real HTTP
router with separate caller credentials and a disposable database.
It does not leave the change-request example running as an HTTP service.
1. Check your first request type
Section titled “1. Check your first request type”Copy the supplied example into a working directory so your edits leave the original unchanged, then check it:
mkdir -p tutorial-workcp -R products/breg/acceptance/asset-site-placement-change-requests tutorial-work/asset-correctionsbregctl check tutorial-work/asset-correctionsThe check succeeds, prints the compiled configuration’s digest, and lists eighteen findings:
seven access.profile.no_required_scope and eleven access.profile.unrestricted_collection.
The block shows the first of each kind and omits the rest:
check succeededrevision: sha256:<configuration-digest>finding access.profile.no_required_scope at entities[id=asset-item].accessProfiles[id=asset-operator].requiredScopes: no scope restricts who may select this profile; any authenticated principal satisfying its purpose and row claims qualifies. Add a required scope unless this is intendedfinding access.profile.unrestricted_collection at entities[id=asset-item].accessProfiles[id=asset-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 accessFindings are advisories, not failures.
The example’s asset-operator and site-planner profiles admit any authenticated caller with the
right purpose and can list every row, and the correction submitter and reviewer can list every
row they reach.
The four correction profiles require scopes.
Leave the findings as they are for this tutorial; --deny-findings turns them into failures when
you want a project held to that bar.
You have a valid request type without writing server code. No database has changed yet. Ask the CLI to describe what it compiled:
bregctl --format json explain change-requests tutorial-work/asset-corrections \ > tutorial-work/change-requests.jsonpython3 - tutorial-work/change-requests.json <<'PY'import jsonimport sysexplanation = json.load(open(sys.argv[1]))["explanation"]request = explanation["requests"][0]print(request["requestEntity"], request["requestRoute"])for stage in request["stages"]: print(stage["id"], stage["approvals"])for action in request["actions"]: print(action["operation"], action["stage"] or "-", ",".join(action["preconditions"]))PYplacement-correction-request placement-correction-requestsreview 1final-approval 1submit_request - Idempotency-Key,If-Matchrevise_request - Idempotency-Key,If-Matchcancel_request - Idempotency-Key,If-Matchapply_request - Idempotency-Key,If-Match,proposalVersion,effectDigestapprove_request review Idempotency-Key,If-Match,proposalVersion,effectDigestreject_request review Idempotency-Key,If-Match,proposalVersion,effectDigestrequest_revision review Idempotency-Key,If-Match,proposalVersion,effectDigestapprove_request final-approval Idempotency-Key,If-Match,proposalVersion,effectDigestreject_request final-approval Idempotency-Key,If-Match,proposalVersion,effectDigestrequest_revision final-approval Idempotency-Key,If-Match,proposalVersion,effectDigestThe example requires one approval at each of two stages.
Every action needs an Idempotency-Key and an If-Match; a decision or an apply also binds to
the proposalVersion and effectDigest the caller read.
The JSON file also lists the compiled effects, the planner and application policy, the grants of each role, and the request bounds.
A planner is the script that computes a request’s effects when they depend on the request’s own
content, declared in place of a fixed effects list; this example declares effects, so the JSON
reports its planner kind as declarative.
See plan effects with a Rhai script
for the alternative.
2. Read the rule you are testing
Section titled “2. Read the rule you are testing”Open registry.yaml in tutorial-work/asset-corrections.
The placement-correction-request entity has its own fields: a placement reference, a proposed site,
and a reason.
The relevant configuration, expanded for readability, is:
changeRequest: retention: mode: operator_erase effects: - target: fromField: placement operation: patch set: site: fromField: proposed-site review: stages: - id: review approvals: 1 excludeSubmitter: true - id: final-approval approvals: 1 excludeSubmitter: trueThe request stores a proposal separately from the placement record.
Its configured effect says which placement field changes when the request is applied.
Submitting and approving the request do not apply that effect.
retention.mode defaults to retain, which keeps every proposal version and refuses operator
erasure.
The example selects operator_erase so an operator can later erase the detail of a finished
request with an explicit command; nothing is deleted on a schedule.
The access profiles separate the work. Each grant lists the operations its role may perform on the request entity:
| Profile | Operations | Responsibility |
|---|---|---|
correction-submitter | create, get, patch, submit_request, revise_request, cancel_request | Draft, edit, submit, and if asked, revise the request |
correction-reviewer | get, list, approve_request, reject_request, request_revision | Decide the review stage |
correction-supervisor | get, approve_request, reject_request, request_revision | Decide the final-approval stage |
correction-applier | get, apply_request | Apply the approved proposal to the placement |
A reviewer’s grant names the stages it may decide under reviewStages, with the placement fields
it may see while deciding.
The applier’s grant names the placement under applyTargets.
Each stage excludes the submitting principal from approving.
That rule does not require a different person for every stage: access profiles and stage grants
determine which other principals can approve.
The example supplies different identities for the two reviewers.
On asset-placement, changeControl.requiredFor: [patch] makes patching a controlled operation.
The ordinary placement grants omit patch, and the generated API has no PATCH route for
placements at all: the only way to change a placement’s site is an applied request.
Those grants carry requestPresence instead, so an operator reading a placement sees that a
correction is pending.
3. Run the approval workflow
Section titled “3. Run the approval workflow”In a second terminal, at the root of the same clone, start the local quickstart.
You are borrowing its disposable PostgreSQL cluster, which the runner reaches over TLS with the
quickstart’s certificate authority.
The --installed flag makes the launcher use the breg, bregctl, and mint you installed
rather than building them from the checkout.
Starting the quickstart replaces its previous products/breg/quickstart/.run directory.
Do not run two quickstarts in the same clone.
Stopping the launcher removes its database container and the records inside it.
Keep real data and production credentials out of this tutorial.
products/breg/quickstart/run.sh --installedThe launcher first prints == Using installed breg, bregctl, and mint from PATH with the three
paths it resolved.
Wait for this line, then leave the terminal running:
Base Registry Engine generic quickstart is ready.Return to your first terminal. Create a Python environment for the example runner’s YAML support:
uv venv tutorial-work/venvuv pip install --python tutorial-work/venv/bin/python 'PyYAML==6.0.2'export PATH="$PWD/tutorial-work/venv/bin:$PATH"Success installs PyYAML into your working directory rather than changing your system Python.
Create a private connection file from the quickstart’s generated credentials, so the database password never enters your shell history:
The file grants administrative access to the disposable cluster. Do not share or commit it, and do not substitute a production database. The example runner sources the file as shell code, so use only the file you create yourself.
python3 - "$PWD/products/breg/quickstart/.run" tutorial-work/test.env <<'PY'from pathlib import Pathfrom urllib.parse import urlsplitimport shleximport sysrun = Path(sys.argv[1])connection = urlsplit((run / "secrets/runtime-database-url").read_text().strip())password = (run / "secrets/database-password").read_text().strip()url = f"postgresql://postgres:{password}@localhost:{connection.port}/postgres"values = { "BREG_TEST_DATABASE_URL": url, "BREG_TEST_TLS_CA_PEM_PATH": str(run / "tls/ca.pem"),}target = Path(sys.argv[2])target.touch(mode=0o600)target.write_text("".join( f"export {name}={shlex.quote(value)}\n" for name, value in values.items()))PYproducts/breg/scripts/test-change-request-examples.sh --installed \ --env tutorial-work/test.env --asset-project "$PWD/tutorial-work/asset-corrections"The --installed flag makes the runner use the same installed breg and bregctl.
Look for these lines in its output:
running change-request fixture: asset-site-placement-change-requestschange-request fixture passed: asset-site-placement-change-requestsrunning change-request fixture: publicschema-household-change-requestschange-request fixture passed: publicschema-household-change-requestsrunning change-request fixture: person-name-change-rhaichange-request fixture passed: person-name-change-rhaiThe runner checks and tests your copy, the unchanged household example, and a third example whose effects a Rhai planner computes. Each run creates its own test databases and roles and removes them on exit. The quickstart’s generic registry stays separate from those test databases.
Open tests/journeys.yaml in your copy.
A journey is a sequence of API calls with the status and fields expected from each response.
This one creates an asset, two sites, and a placement, drafts the correction, submits it, collects
both approvals, and applies it.
On a running server, the same journey is this sequence of HTTP calls:
| Journey steps | Caller | HTTP call |
|---|---|---|
| Create the asset, both sites, and the placement | asset-operator | POST /v1/records/assets, /v1/records/sites, and /v1/records/placements |
| Create and edit the draft request | correction-submitter | POST /v1/records/placement-correction-requests, then PATCH /v1/records/placement-correction-requests/{id} with the record’s ETag |
| Submit | correction-submitter | POST /v1/records/placement-correction-requests/{id}/actions/submit |
Approve the review stage | correction-reviewer | POST /v1/records/placement-correction-requests/{id}/actions/stages/review/approve |
Approve the final-approval stage | correction-supervisor | POST /v1/records/placement-correction-requests/{id}/actions/stages/final-approval/approve |
| Apply | correction-applier | POST /v1/records/placement-correction-requests/{id}/actions/apply |
Before each action, the journey gets the request as the next caller.
The response’s data.request.actions[] lists what that caller may do next, each with its href,
its own ifMatch, and, for decisions and apply, the proposalVersion and effectDigest it binds
to.
The journey’s etagRef, proposalVersionRef, and effectDigestRef take those values from the
captured response, and a client making HTTP calls itself reads the same members.
The record’s ordinary ETag header edits the draft; it is not an action’s ifMatch.
4. Make the second stage your own
Section titled “4. Make the second stage your own”Rename final-approval to operations-approval in your copied registry.yaml, in two places:
the stage’s id and the correction-supervisor grant’s reviewStages entry.
Then change the stage of the approve-final-stage step in tests/journeys.yaml to the same
name.
The step and capture identifiers can keep their original names.
Your stage, grant, and journey step now agree on who approves what:
# Under changeRequest.review.stages:- {id: operations-approval, approvals: 1, excludeSubmitter: true}# The correction-supervisor grant:- entity: placement-correction-request operations: [get, approve_request, reject_request, request_revision] readableFields: [placement, proposed-site, reason] reviewStages: - stage: operations-approval targets: - {entity: asset-placement, readableFields: [site], rowBoundaries: []}# The approve-final-stage step in tests/journeys.yaml:request: operation: approve_request stage: operations-approvalRun the structural check and the database journey again:
bregctl check tutorial-work/asset-correctionsproducts/breg/scripts/test-change-request-examples.sh --installed \ --env tutorial-work/test.env --asset-project "$PWD/tutorial-work/asset-corrections"The check reports a different configuration digest with the same findings, and all three fixtures pass again. You changed the stage name, its authority grant, and the caller’s action without changing server code. For a new stage rather than a rename, you would also add its GET and approval steps to the journey.
5. Check that a direct-write bypass is refused
Section titled “5. Check that a direct-write bypass is refused”In your copied registry.yaml, find the asset-operator grant for asset-placement.
Temporarily add patch to its operations:
operations: [create, get, list, patch]bregctl check tutorial-work/asset-correctionsThe check fails with change_control.direct_write_grant and the message
a controlled mutation operation cannot remain directly granted.
This is an expected refusal: a grant cannot bypass the placement’s changeControl rule.
Remove patch from that grant and rerun the check; it succeeds again.
Cleanup
Section titled “Cleanup”The example runner already removed its temporary databases, roles, and credentials.
In the second terminal, press Ctrl+C to stop the quickstart services and remove its container.
Your copied project remains in tutorial-work/asset-corrections.
To keep that project but remove the tutorial connection file:
rm -f tutorial-work/test.envThe quickstart leaves keys, credentials, and logs under its .run directory.
After stopping the services, remove that directory if you no longer need those files.
This also deletes the quickstart’s generated project, but not your separate copy.
rm -rf products/breg/quickstart/.runWhat you built
Section titled “What you built”You tested a proposal with its own schema, two approval stages, and a controlled effect on an existing record. You changed one stage and verified that an ordinary write grant cannot override change control. These are local authoring and database tests; packaging and activating the configuration for your deployment are separate steps.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Next move |
|---|---|
check prints findings | Findings are advisories; the first line is the verdict. Pass --deny-findings when you want them to fail the check. |
| The quickstart cannot start Docker or a service | Start Docker and check the named prerequisite. Inspect .run/logs/ without sharing secrets. |
| The database connection is refused or the certificate fails | Keep the quickstart running and recreate test.env after restarting it. Each run has new ports and certificates. |
Python cannot import yaml | Repeat the virtual environment installation and PATH assignment in your first terminal. |
Renaming the stage makes check fail | Match the stage id to the supervisor’s reviewStages entry. |
The runner reports test.step.failed | Its path journeys[0].steps[<n>] counts the steps of tests/journeys.yaml from zero. Check that step’s stage and expected status. |
change-requests is not a recognized explain subject | Your bregctl predates the checkout. Confirm that bregctl --version matches the checkout’s tag, then reinstall or clone again so they agree. |
command not found for bregctl or mint | The installers in Create and query your first registry place all three binaries in ~/.local/bin. Add that directory to PATH in this terminal, or rerun the installer you skipped. |
- Declare change requests and actions for every
member of
changeRequestandchangeControl. - Base Registry Engine API reference for the
seven request states, the action routes, and the
request.actions[]shape. - Deploy a registry to package and activate your
project, to review a later revision with
bregctl diff, and to erase proposal detail underoperator_erase. - Open
products/breg/CHANGE_REQUEST_EXAMPLES.mdin your clone for the household example and stale-proposal recovery. - Configure Registry Mint when connecting callers to your own deployment.