Skip to content
Registry StackDocsv0.34.0

Create and query your first registry

For the data publisher

View as Markdown

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.

Outcome
A record created and updated over HTTP, with refused requests showing authentication, field permissions, version checks, and required fields at work.
Time
About 15 minutes, plus the image download
Level
Local evaluation only
Prerequisites
Linux amd64 or arm64, or macOS on Apple SiliconA Bash or zsh shellRunning Dockercurl 7.76 or laterPython 3, to format JSON responsesAn editor

Install breg and bregctl. The local development command starts the maintained stock identity provider and registers the tutorial clients:

Terminal window
curl -fsSL https://github.com/registrystack/registry-stack/releases/latest/download/breg-install.sh | bash
bregctl --version

The 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.

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/.

Terminal window
mkdir -p tutorial-work
bregctl init tutorial-work/project
Initialized 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
resolves

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 profiles
  • modules/record-notes/module.yaml: a reusable module, one optional field
  • tests/journeys.yaml: the requests a test run replays
  • dev-clients.yaml: the local callers and their tokens
  • runtime.example.yaml: an operator’s runtime configuration
  • README.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 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 project as a registry:

Terminal window
bregctl dev tutorial-work/project

The 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.

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:

Terminal window
registry_url=http://127.0.0.1:8090
bregctl dev token operator tutorial-work/project
authorization_header=tutorial-work/project/.breg/dev/secrets/operator.header

bregctl 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 records as the operator profile; accessProfile names the permissions the request wants, and BReg checks the token against them:

Terminal window
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:

Terminal window
curl --silent --show-error \
--output tutorial-work/problem.json --write-out 'HTTP %{http_code}\n' \
"$registry_url/v1/records/records?accessProfile=operator"
HTTP 404

Open 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 the business record and save the response:

Terminal window
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 201

Read the response:

Terminal window
python3 -m json.tool tutorial-work/created.json

The 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.

Ask for labels only:

Terminal window
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.tool

Each 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:

Terminal window
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 400

The 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:

Terminal window
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.tool

The 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.

Read the created record and save its HTTP headers:

Terminal window
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.tool

The response still shows "revisionIdentifier": "1", and its ETag header identifies that version. Save the exact value, including its quotes:

Terminal window
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:

Terminal window
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.tool

The 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:

Terminal window
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 412

The problem code is precondition.failed, the label remains North Quay Engineering Ltd, and a new idempotency key does not bypass that check.

Omit the required code field:

Terminal window
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 400

The 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 services:

Terminal window
bregctl dev stop tutorial-work/project

The 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:

Terminal window
bregctl dev stop tutorial-work/project --remove

Either 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.

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.

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.

SymptomNext move
bregctl or breg is not foundAdd the installer’s directory, ~/.local/bin unless you changed it, to PATH in this terminal.
bregctl dev refuses over a reported versionThe 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 portSomething 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 readyRead 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.conflictThe 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.conflictThe code is already taken; codes are unique in this project. Choose a new code and a new key.
You opened a fresh terminalRun the registry_url and credentials assignments from Get a token again. The header file and tutorial-work directory are still there.

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.