Released docs. You are viewing the documentation published with v0.34.0. Development docs are available at Latest.
Create and query your first registry
For the data publisher
If you are a data publisher evaluating
Base Registry Engine (BReg), start with a small
business directory.
You will generate a registry project, run it on your machine, create and update a business record,
and try requests the server refuses.
Everything you keep goes into one directory, tutorial-work.
Install Base Registry Engine
Section titled “Install Base Registry Engine”Install breg and bregctl. The local development command starts the maintained stock identity
provider and registers the tutorial clients:
curl -fsSL https://github.com/registrystack/registry-stack/releases/latest/download/breg-install.sh | bashbregctl --versionThe installer checks both binaries against the release SHA256SUMS before anything reaches
~/.local/bin, and installs them together or not at all.
Keep that directory on your PATH.
The URL takes the latest release, and a deployment pins a version instead.
The command pipes a script from GitHub into bash; | less in place of | bash reads it first.
Verify the release as described in OpenSSF and release trust,
then rerun the installer with BREG_ASSET_DIR pointing at the verified directory.
Create a project
Section titled “Create a project”Open a terminal in a directory of your choice and generate a project.
Keep this terminal in the same directory for the rest of the tutorial; every path starts with
tutorial-work/.
mkdir -p tutorial-workbregctl init tutorial-work/projectInitialized a registry project. 6 artifacts written. revision sha256:<digest>
README.md dev-clients.yaml modules/record-notes/module.yaml registry.yaml runtime.example.yaml tests/journeys.yaml
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.
Next: 1. read tutorial-work/project/README.md, then run 'bregctl check tutorial-work/project' 2. leave the findings above as they are; the example operator profile lists a whole collection and the example evidence-source profile looks up any record, both on purpose, and tutorial-work/project/README.md says where to narrow them 3. replace canonicalBaseIri in tutorial-work/project/registry.yaml before you build a production package; the example value is a reserved .invalid name that never resolvesWhat init wrote
Section titled “What init wrote”A registry project declares one registry in YAML, and init compiled this one before printing its
revision, the digest of the compiled model.
The six files are:
registry.yaml: entities, fields, and access profilesmodules/record-notes/module.yaml: a reusable module, one optional fieldtests/journeys.yaml: the requests a test run replaysdev-clients.yaml: the local callers and their tokensruntime.example.yaml: an operator’s runtime configurationREADME.md: what each file holds and what to change
tutorial-work/project/README.md describes each one at length; open it.
The record entity in registry.yaml carries the required code and label fields you will write,
plus an optional group and status: the business directory is the example you supply, not a built-in
type.
A finding is advice the compiler attaches to a result that succeeded: an error stops a command, a
finding does not.
Both findings here are expected, as the second step under Next: says, and every later command
repeats them.
The local callers
Section titled “The local callers”The operator and reader callers in dev-clients.yaml are ready to use with no edits, and
this tutorial acts as operator.
The updated candidate also includes a dedicated source client for later Evidence integration.
Local clients and their tokens
explains what a client entry declares and why naming a profile there grants nothing on its own.
Start the registry
Section titled “Start the registry”The registry stores its records in a Docker volume on your machine, and the private keys it
generates live in tutorial-work/project/.breg/dev/, an owner-only directory that stays out of
version control.
Nothing here is configured for anyone else’s data.
Start the project as a registry:
bregctl dev tutorial-work/projectThe first start downloads the pinned PostgreSQL image, so it takes longer than the later ones. The command returns once the registry answers. Its report names the service URLs, package revision, and private credential file paths for each client.
dev started PostgreSQL and the maintained stock identity provider in containers, registered the
clients from dev-clients.yaml under a fresh key each, built a package, replayed
tests/journeys.yaml against a
throwaway database, activated the package, and started BReg at the address the report shows.
The services keep running after the command returns, so this one terminal is enough. Run the same command again at any time to print the report again.
Get a token
Section titled “Get a token”Save the registry address, then ask the local issuer for a token as the operator client. The
command writes a header file that curl can send:
registry_url=http://127.0.0.1:8090bregctl dev token operator tutorial-work/projectauthorization_header=tutorial-work/project/.breg/dev/secrets/operator.headerbregctl dev token requests a fresh token through the registered client and reports the private
header-file path. The token carries the operator scopes and claims and lasts five minutes.
The header file is readable only by your user; keep it out of version control and support messages.
If a request returns 401 after a pause, renew the token and retry.
Read the registry
Section titled “Read the registry”Read the records as the operator profile; accessProfile names the permissions the request wants,
and BReg checks the token against them:
curl --silent --show-error --fail-with-body \ --header @"$authorization_header" \ "$registry_url/v1/records/records?accessProfile=operator" | python3 -m json.tool{ "items": [], "meta": { "datasetIdentifier": "generic-registry", "entityTypeIdentifier": "record", "registryIdentifier": "generic-registry" }, "pageInfo": { "nextCursor": null }}The registry is empty, and the empty list is a successful read.
The response is the Registry Record envelope every route on this API returns: records in items,
the registry, dataset, and entity type in meta, and paging in pageInfo.
Try the same request without the header:
curl --silent --show-error \ --output tutorial-work/problem.json --write-out 'HTTP %{http_code}\n' \ "$registry_url/v1/records/records?accessProfile=operator"HTTP 404Open tutorial-work/problem.json: its code is resource.not_found.
BReg conceals a protected route from an unauthenticated caller by answering as if the route did not
exist, and naming a profile in the query granted nothing.
Create a record
Section titled “Create a record”Create the business record and save the response:
curl --silent --show-error --fail-with-body \ --header @"$authorization_header" \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: tutorial-create-1' \ --data '{"data":{"code":"DEMO-002","label":"North Quay Engineering"}}' \ --output tutorial-work/created.json --write-out 'HTTP %{http_code}\n' \ "$registry_url/v1/records/records?accessProfile=operator"HTTP 201Read the response:
python3 -m json.tool tutorial-work/created.jsonThe server generates the identifier, shown as <record-id>:
{ "data": { "domainData": { "code": "DEMO-002", "group": null, "label": "North Quay Engineering", "status": null }, "recordIdentifier": "<record-id>", "revisionIdentifier": "1", "snapshot": "<snapshot-token>" }, "meta": { "datasetIdentifier": "generic-registry", "entityTypeIdentifier": "record", "registryIdentifier": "generic-registry" }}A single record sits under data; a list carries the same fields in each item.
group and status are optional and read as null until a record supplies them.
data.snapshot is an opaque token for a later :snapshot read, described in the
Base Registry Engine API reference.
Run the create command again unchanged: you get 201 with the same identifier and revision, not a
second record, because Idempotency-Key names one write.
Use a different key for a different write.
Choose which fields to read
Section titled “Choose which fields to read”Ask for labels only:
curl --silent --show-error --fail-with-body \ --header @"$authorization_header" --get \ --data-urlencode 'accessProfile=operator' \ --data-urlencode '$select=label' \ --data-urlencode '$top=10' \ "$registry_url/v1/records/records" | python3 -m json.toolEach item’s domainData now contains only label.
The identifiers stay in the envelope: $select names configured fields only and refuses recordIdentifier.
Reading a field does not permit filtering by it.
Try a filter on label:
curl --silent --show-error \ --header @"$authorization_header" --get \ --data-urlencode 'accessProfile=operator' \ --data-urlencode "\$filter=label eq 'North Quay Engineering'" \ --output tutorial-work/problem.json --write-out 'HTTP %{http_code}\n' \ "$registry_url/v1/records/records"HTTP 400The problem code is query.invalid.
The operator grant for record in tutorial-work/project/registry.yaml carries three separate
permission lists:
readableFields: [code, label, group, status]writableFields: [code, label, group, status]filterableFields: [code, status]Repeat the request on code, which is filterable, and match the code you created, DEMO-002:
curl --silent --show-error --fail-with-body \ --header @"$authorization_header" --get \ --data-urlencode 'accessProfile=operator' \ --data-urlencode "\$filter=code eq 'DEMO-002'" \ "$registry_url/v1/records/records" | python3 -m json.toolThe request succeeds and items holds the one record you created: label is readable and writable
but not filterable, so $select may name it and $filter may not.
Filtering on code eq 'North Quay Engineering' would also succeed, with an empty items, because a
filter that matches nothing is a successful read, not a refusal.
Update the record
Section titled “Update the record”Read the created record and save its HTTP headers:
record_id=$(python3 -c 'import json; print(json.load(open("tutorial-work/created.json"))["data"]["recordIdentifier"])')curl --silent --show-error --fail-with-body \ --header @"$authorization_header" \ --dump-header tutorial-work/record.headers \ "$registry_url/v1/records/records/$record_id?accessProfile=operator" | python3 -m json.toolThe response still shows "revisionIdentifier": "1", and its ETag header identifies that version.
Save the exact value, including its quotes:
record_etag=$(awk 'tolower($1) == "etag:" {print $2}' tutorial-work/record.headers | tr -d '\r')Change the label with JSON Patch; If-Match makes the server compare that ETag with the record’s
own before writing:
curl --silent --show-error --fail-with-body \ --header @"$authorization_header" \ --header 'Content-Type: application/json-patch+json' \ --header 'Idempotency-Key: tutorial-patch-1' \ --header "If-Match: $record_etag" \ --request PATCH \ --data '[{"op":"replace","path":"/data/label","value":"North Quay Engineering Ltd"}]' \ "$registry_url/v1/records/records/$record_id?accessProfile=operator" | python3 -m json.toolThe response has the same identifier, "revisionIdentifier": "2", and label North Quay Engineering Ltd.
Patch paths address the object you sent at creation, so /data/label names the label;
domainData in responses is not a patch target.
Try a different update while still using the old ETag:
curl --silent --show-error \ --header @"$authorization_header" \ --header 'Content-Type: application/json-patch+json' \ --header 'Idempotency-Key: tutorial-patch-2' \ --header "If-Match: $record_etag" \ --request PATCH \ --data '[{"op":"replace","path":"/data/label","value":"North Quay Engineering Group Ltd"}]' \ --output tutorial-work/problem.json --write-out 'HTTP %{http_code}\n' \ "$registry_url/v1/records/records/$record_id?accessProfile=operator"HTTP 412The problem code is precondition.failed, the label remains North Quay Engineering Ltd, and a new
idempotency key does not bypass that check.
Try an invalid record
Section titled “Try an invalid record”Omit the required code field:
curl --silent --show-error \ --header @"$authorization_header" \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: tutorial-missing-code' \ --data '{"data":{"label":"South Harbour Logistics"}}' \ --output tutorial-work/problem.json --write-out 'HTTP %{http_code}\n' \ "$registry_url/v1/records/records?accessProfile=operator"HTTP 400The problem code is request.invalid, and no record was created.
The code field carries required: true in tutorial-work/project/registry.yaml, and that one
setting both validated this request and shaped the project’s generated request schema.
Stop the registry
Section titled “Stop the registry”Stop the services:
bregctl dev stop tutorial-work/projectThe report’s status reads stopped.
The container and its volume stay, with your records and audit history, and the start command from
Start the registry brings the same registry back.
To discard the records as well, stop with --remove:
bregctl dev stop tutorial-work/project --removeEither way, keep tutorial-work/: the next tutorial edits the project inside it.
After a plain stop, the next start refuses an edited project while the records exist.
After --remove, the next start builds the project as it stands, edits included, and generates new
keys if the project changed.
What you built
Section titled “What you built”You generated a project, started it as a registry, created and updated a record, and saw
authentication, query permissions, version checks, and required fields affect real requests.
All of that came from the YAML in tutorial-work/project.
Troubleshooting
Section titled “Troubleshooting”Renew an expired token
Section titled “Renew an expired token”The local token lasts five minutes. Run the command from Get a token again; it replaces the header file. If it fails, the registry is stopped: start it again with the command from Start the registry, then renew.
Other problems
Section titled “Other problems”| Symptom | Next move |
|---|---|
bregctl or breg is not found | Add the installer’s directory, ~/.local/bin unless you changed it, to PATH in this terminal. |
bregctl dev refuses over a reported version | The breg it resolved comes from another release than bregctl. Install both from the same release, or put the matching build first on PATH. |
bregctl dev refuses a port | Something else listens on 8090, 8091, or 55432. Pass --breg-port, --issuer-port, or --database-port on the first start; later starts keep the ports you chose. |
bregctl dev fails before it reports ready | Read the refusal: it names the check that failed, such as test.step.failed with the journey step’s own message. Otherwise check that Docker is running. The private logs are under tutorial-work/project/.breg/dev/logs/; do not share the credential or secret files beside them. |
A create returns 409 with code idempotency.conflict | The key was already used with a different body. Reuse the exact body, or choose a new key for a new write. |
A create returns 409 with code mutation.conflict | The code is already taken; codes are unique in this project. Choose a new code and a new key. |
| You opened a fresh terminal | Run the registry_url and credentials assignments from Get a token again. The header file and tutorial-work directory are still there. |
- Extend a registry with a module to change a field in the project you generated.
- Review changes before updating a registry to test a configurable approval workflow.
- How a configured registry works for the compile model behind what you saw.
- Query a registry from Python and Node to make the same requests from an application.
- What you need to run Base Registry Engine before you plan a deployment.
With the updated candidate, the dedicated source client is ready for the existing lookup profile.
To add Evidence
while keeping records you have edited, stop normally and use
bregctl dev export-client --help for the explicit credential handoff.
With matching candidate Evidence tools, custom models can add a dedicated lookup
after use through evidencectl source add; it prepares an explicit policy successor
without resetting the retained records.