Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.26.1.
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 start a local registry, create a business record, change its label, and try requests the server refuses.
The records use two required fields, code and label, plus an optional group and status;
the business directory is the example you supply, not a built-in type.
Everything you keep goes into one directory, tutorial-work, and the next tutorial,
Extend a registry with a module, continues in that directory.
Install Base Registry Engine
Section titled “Install Base Registry Engine”Install breg and bregctl, then the Evidence Gateway toolset, because Registry Mint ships in it and
the launcher runs Mint to issue the local access tokens:
curl -fsSL https://github.com/registrystack/registry-stack/releases/latest/download/breg-install.sh | bashcurl -fsSL https://github.com/registrystack/registry-stack/releases/latest/download/evidencectl-install.sh | bashbregctl --versionmint --versionThe URLs name latest, which this tutorial takes on purpose so an evaluation starts on the newest
release; a deployment pins a version instead, so the binary that was tested is the binary that runs.
Each installer checks its binaries against the release SHA256SUMS before anything reaches
~/.local/bin, and installs them together or not at all.
Keep that directory on your PATH in both terminals.
The commands pipe a script from GitHub straight into bash; replace | bash with | less to read it first.
Before these binaries reach a host that serves anyone else, verify the release as described in
OpenSSF and release trust, then rerun the installer with
BREG_ASSET_DIR pointing at the verified directory.
Get the quickstart files
Section titled “Get the quickstart files”The launcher and its fixtures live in the repository. Clone it at the version you installed:
installed="$(bregctl --version | awk '{print $2}')"git clone --depth 1 --branch "v$installed" https://github.com/registrystack/registry-stack.git breg-tutorialcd breg-tutorialThe project is pre-1.0, so the default branch may carry changes the installed binaries do not implement; pinning the checkout keeps the launcher and the binaries on one contract. Run the commands from this checkout’s root in both terminals.
Start the registry
Section titled “Start the registry”The launcher creates a disposable database and local keys.
Stopping it removes the database container.
Starting it again replaces products/breg/quickstart/.run/, including its keys, tokens, and logs.
In the first terminal, start the services:
products/breg/quickstart/run.sh --installedThe flag tells the launcher to use the breg, bregctl, and mint on your PATH rather than
compiling them from the checkout.
Wait for this line; setup output is omitted:
Base Registry Engine generic quickstart is ready.Leave this terminal running.
The launcher has created one record with code QS-001, and BReg and Mint listen on loopback addresses
it printed, with ports chosen for this run.
Read the first record
Section titled “Read the first record”In the second terminal, save the address and prepare an authorization header file:
registry_run="$PWD/products/breg/quickstart/.run"registry_url=$(cat "$registry_run/breg-origin")umask 077mkdir tutorial-worksed 's/^/Authorization: Bearer /' "$registry_run/secrets/operator-token" \ > tutorial-work/authorization.headerThe header file holds a token and 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 records using the operator access profile:
curl --silent --show-error --fail-with-body \ --header @tutorial-work/authorization.header \ "$registry_url/v1/records/records?accessProfile=operator" | python3 -m json.toolThe items array contains one record, and its domainData object holds the configured fields:
{ "code": "QS-001", "group": null, "label": "Quickstart example record", "status": null}group and status are optional fields of the same entity and read as null until a record supplies them.
Beside domainData, recordIdentifier and revisionIdentifier identify the record and its version,
and meta names the registry, dataset, and entity type.
What happened: you read a Registry Record envelope, the shape every response on this API shares,
with the record’s fields and identifiers in the item and meta saying where the record lives.
You chose an access profile, a named set of permissions a token may use, with the accessProfile query parameter.
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.
What happened: 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 @tutorial-work/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.
data.snapshot appears only on create, patch, and tombstone responses: an opaque token for a later
:snapshot read, described in the Base Registry Engine API reference.
Run the create command again unchanged, then read the response again.
You get 201 with the same identifier and revision, not a second record.
What happened: Idempotency-Key identifies a single write, so a client that lost the first response
can retry without creating a duplicate; 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 @tutorial-work/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 @tutorial-work/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.
Open products/breg/quickstart/.run/project/registry.yaml and find the operator grant for record:
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.
The filter compares the field to a literal, so the value changes with the field:
curl --silent --show-error --fail-with-body \ --header @tutorial-work/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, with recordIdentifier,
revisionIdentifier, and domainData in each item.
Filtering on code eq 'North Quay Engineering' would also succeed, with an empty items: no
record carries that code, and a filter that matches nothing is a successful read, not a refusal.
What happened: a grant carries three separate permission lists, and label is readable and writable
but not filterable, so $select may name it and $filter may not.
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 @tutorial-work/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 refuses the update if the record changed since your read:
curl --silent --show-error --fail-with-body \ --header @tutorial-work/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 @tutorial-work/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.
What happened: the ETag names the version you read and If-Match makes the server compare it before
writing, so a stale client cannot overwrite a change it never saw, 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 @tutorial-work/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; no record was created.
In the project’s registry.yaml, find required: true on the code field.
What happened: the same required: true that validated this request also shapes the project’s
generated request schema, so the model decides what a record needs, not application code.
Stop the services
Section titled “Stop the services”Pressing Ctrl+C in the first terminal stops BReg and Mint and removes the PostgreSQL container,
including the records you created.
Stop the launcher now.
The .run/ files remain until the next launch and contain disposable secrets.
Keep tutorial-work/: the next tutorial creates a project inside it, and the checkout’s .gitignore
excludes the directory, so the header file stays out of commits.
What you built
Section titled “What you built”You created and updated a record, retried a write, narrowed a read, and saw authentication, query permissions, version checks, and required fields affect real requests. All of that came from one project the launcher compiled; the next tutorial puts a copy in your hands.
Troubleshooting
Section titled “Troubleshooting”Renew an expired token
Section titled “Renew an expired token”The local token lasts five minutes. While Mint is still running, renew it and replace the header file:
umask 077mint token \ --url "$(cat "$registry_run/mint-origin")/token" \ --client-id generic-quickstart \ --key "$registry_run/keys/operator/signing-p256-private-jwk" \ > "$registry_run/secrets/operator-token" &&sed 's/^/Authorization: Bearer /' "$registry_run/secrets/operator-token" \ > tutorial-work/authorization.headerSuccess prints nothing; retry the request. If renewal fails, check that the first terminal is still running.
Other problems
Section titled “Other problems”| Symptom | Next move |
|---|---|
The launcher stops because breg, bregctl, or mint is not on PATH | Add the installers’ directory, ~/.local/bin unless you changed it, to PATH in that terminal. |
| A service does not become ready | Inspect .run/logs/ under the quickstart directory. Do not share tokens, keys, or database URL files. |
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 second terminal | Run the registry_run and registry_url assignments again. Keep the existing tutorial-work directory. |
- Extend a registry with a module to create a project of your own and change a field.
- 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.
- Evaluate Base Registry Engine for the operating requirements before you commit to it.