Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.26.1.
You have a model and its profiles, and you want proof that the project behaves before it reaches a database.
This page covers the offline checks first: check compiles the project and reports findings, explain prints what the compiler derived, and generate writes the artifacts a client will see.
It then covers journeys, the scenarios in tests/journeys.yaml that bregctl test replays over HTTP against a throwaway database, because a row boundary, a query bound, or a review workflow is only proven by a request that runs.
At the end you know what each command proves, what only a journey can prove, and what a failure of each looks like.
Check the project
Section titled “Check the project”bregctl check ./my-registrycheck compiles the project without a database and prints the compiled revision, a content digest of everything the compiler read:
check succeededrevision: sha256:<digest>finding access.profile.unrestricted_collection at 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 accessTwo people holding the same revision hold the same project.
An error is a declaration the compiler cannot compile, printed as error <code> at <path>: <message>, and the command exits with status 1.
A finding is an advisory attached to a document path: the project compiles, but a reviewer must read it.
The project init wrote reports the access.profile.unrestricted_collection finding on purpose, because its operator profile lists every record.
Two flags change what counts as a failure, and a review gate wants both:
bregctl check ./my-registry --deny-findingsbregctl check ./my-registry --production--deny-findings turns every finding into an error, so the first command prints error access.profile.unrestricted_collection at ... and exits with status 1 until the finding is resolved or the profile is changed.
--production applies the production closure rules: package identity, a lock entry with a pinned digest for every module, a loaded source for every lock, and a webhook for every event must all be present, and a project missing one fails with package.identity.required, module.lock.missing, module.lock.digest_required, module.source.required, or event.delivery.required.
The project init wrote already carries all of them, so check --production succeeds on it.
explain and generate accept --production as well; --deny-findings belongs to check alone.
Explain what the compiler derived
Section titled “Explain what the compiler derived”bregctl explain model ./my-registrybregctl explain routes ./my-registrybregctl explain queries ./my-registryexplain takes one subject, prints the revision and findings the way check does, then the report: JSON for every subject except access, whose report is written for reading.
model prints every entity, field, and profile as compiled.
routes prints every HTTP route with its method, path, operation, and the profiles that may select it, including the default.
queries prints, per entity and operation, the fields a response may carry, the filterable fields with example $filter expressions, and the query bounds a request stays within.
access prints the compiled grants per profile in a human-readable form and, with --scenario, previews admission; control access per profile covers it.
change-requests and actions are covered on declare change requests and actions, and events on author a registry project.
Read explain queries before you write a client or a journey: a request that filters on a field the profile cannot filter is refused, not empty.
Generate artifacts
Section titled “Generate artifacts”mkdir -p ./generatedbregctl generate openapi ./my-registry --output ./generated/openapibregctl generate schemas ./my-registry --output ./generated/schemasgenerate writes one artifact family into a new directory: openapi, schemas, actions, manifest, metadata, or sql.
The output directory’s parent must exist, and the directory itself must not: a missing parent fails with output.parent.invalid, and an existing directory, or a path containing .., fails with output.destination.invalid, so a run can never overwrite artifacts you already reviewed.
The command creates the directory and writes under a generated/ subdirectory inside it, so the two commands produce ./generated/openapi/generated/openapi.json and ./generated/schemas/generated/schemas/<entity>.schema.json.
Output is deterministic: the same project produces the same bytes, so commit the generated documents and, in review, generate into a fresh directory and diff it against the committed copy.
The running server publishes the same OpenAPI document at /openapi.json and the same schemas under /v1/schemas/.
Findings
Section titled “Findings”These are all the findings the compiler can report, each attached to the document path it applies to:
| Finding | Meaning | What to do |
|---|---|---|
access.profile.anonymous_collection | list is granted to unauthenticated callers, so every row the profile can read is public. | Confirm the whole collection is meant to be public. |
access.profile.data_export | Bulk export is enabled, and disabling it later cannot recall downloaded data. | Review the readable fields and row boundaries first. |
access.profile.higher_classification | A readable field is more sensitive than its entity’s classification. | Verify the profile’s scope and purpose before disclosing it. |
access.profile.no_required_scope | No scope restricts who may select the profile. | Add a required scope unless that is intended. |
access.profile.no_writable_fields | The profile grants create or patch and names no writable field. | List the writable fields, or remove the write operations. |
access.profile.related_disclosure | A read path discloses related records under the root profile rather than the target entity’s own profiles. | Review its fields and the target entity’s access requirements. |
access.profile.revision_history | History can disclose values removed from the current record. | Review historical disclosure separately. |
access.profile.snapshot_history | Snapshot reads reproduce retained historical rows under current authorization. | Review stored-field projection, filters, and row boundaries separately. |
access.profile.unrestricted_collection | The profile can list every row; caller filters are not authorization. | Add a claim-bound row boundary, or accept a registry-wide role knowingly. |
access.profile.writable_row_boundary | patch can change a field a row boundary depends on, within the caller’s allowed values. | Remove it from the writable fields unless moving records is intended. |
manifest_projection.missing | No Registry Manifest projection is declared. | Declare one to clear the finding. check --production passes without it, and package builds a package without one. |
module.lock.digest_missing | A module lock entry has no production digest. | Run project lock before a production package. |
module.lock.missing | A module source has no lock entry. | Run project lock. |
module.source.missing | A lock entry names a module whose source is not loaded. | Restore the module file or remove the lock entry. |
package.identity.missing | No package identity is declared. | Declare one before a production package. |
Under --production, the module.* and package.* findings become errors; manifest_projection.missing is reported only in authoring mode, so resolve it before you leave this page.
Write journeys
Section titled “Write journeys”A journey is a list of steps; each step names an entity, a profile, the claims the synthetic token carries, one request, and the outcome it expects.
bregctl test replays them over real HTTP against an empty PostgreSQL database, binding one credential per step from its own credentials file, so the journey file never holds a credential.
Every entity, profile, field, and claim in a journey is resolved against the compiled project first, so a journey can never reach past what a profile already allows.
The journey init wrote proves the record-reader row boundary by moving a record out of the reader’s rows and counting; these steps add a refusal:
apiVersion: registry.registrystack.org/breg-journeys/v1journeys: - id: record-lifecycle steps: - id: create-record-group entity: record-group accessProfile: operator claims: &operator_claims principal: generic-registry-operator scopes: [registry:generic:operate] purpose: registry-operations request: operation: create data: code: group-a label: Example group expect: outcome: success status: 201 fields: code: group-a capture: example-group - id: create-record entity: record accessProfile: operator claims: *operator_claims request: operation: create data: code: example label: Example record group: recordRef: example-group status: active expect: outcome: success status: 201 capture: example-record - id: retire-record entity: record accessProfile: operator claims: *operator_claims request: operation: patch recordRef: example-record etagRef: example-record changes: - field: status value: retired expect: outcome: success status: 200 fields: status: retired - id: reader-outside-the-claim-sees-nothing entity: record accessProfile: record-reader claims: principal: generic-registry-reader scopes: [registry:generic:read] purpose: registry-reporting directClaims: registry_record_status: active request: operation: get recordRef: example-record expect: outcome: refusal status: 404 problemCode: resource.not_found| Step member | Meaning |
|---|---|
claims | principal, scopes, purpose, and the directClaims row boundaries and claim lookups read. Values are synthetic. |
request.operation | create, get, list, query, lookup, read_path, patch, batch, target_conditions, invoke, or a change-request action: submit_request, approve_request, reject_request, and request_revision with a stage, revise_request, cancel_request, or apply_request. |
recordRef, etagRef | A record and ETag captured by an earlier step. etagRef sends the ETag as If-Match, so a patch fails rather than overwrite a concurrent change. |
data, changes, items, selector and values, path, select, top, count, bbox, input, preconditions | The request body or query for the operation. |
expect | outcome is success or refusal; status is the HTTP status; fields asserts returned values; count asserts the item count; problemCode asserts the problem code of a refusal. |
capture, captureResults | Store the returned record and ETag, or an action’s named results, under a name for later steps. |
A read outside the caller’s boundary returns 404 with resource.not_found, because the server conceals what the caller may not know exists.
Write a refusal step for every boundary you rely on: the compiler cannot prove a row boundary works, only a journey can.
Running the journeys needs more than the project: an empty PostgreSQL database, a runtime configuration, a credentials file binding one credential per step, and a path for the receipt bregctl test writes.
Build a production candidate runs them step by step, and deploy a registry covers the runtime configuration they read.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause and next move |
|---|---|
check passes but check --deny-findings fails. | A finding is present. Read it and either change the project or accept the finding knowingly. |
generate fails with output.parent.invalid. | The output directory’s parent does not exist. Create it, or give a destination inside a directory that exists. |
generate fails with output.destination.invalid. | The output directory already exists, or the path contains ... Give a new path; remove the old directory first if you meant to regenerate. |
| A journey step fails with 404 where you expected data. | The profile’s row boundary or purpose excluded the caller. Compare the step’s claims with the grant, or preview them with explain access --scenario (control access per profile). |
| A journey filters or sorts on a field and is refused. | The profile’s filterableFields or sortableFields do not list it. Check explain queries for the forms the profile may use. |
- Build a production candidate: run
check --productionandbregctl testagainst a PostgreSQL you start, then package and sign. - Deploy a registry: the runtime configuration, database, and token requirements the journeys depend on.
- Base Registry Engine API reference: the routes, query options, and problem documents the journeys exercise.
- Base Registry Engine configuration reference: every journey and project key.