Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/llms.txt
Full corpus: https://docs.registrystack.org/llms-full.txt

# Extend a registry with a module

> Open the registry project you generated, change a field its module contributes, re-pin the module by content digest, and generate the JSON Schema and OpenAPI that carry the change.

import QuickstartMeta from '../../../components/QuickstartMeta.astro';

If you finished [Create and query your first registry](../first-breg/), you are a data publisher with the
installed binaries and a `tutorial-work` directory holding the project you generated.
In this tutorial you change a field that a module in that project contributes, re-pin the module so the
project accepts the change, and generate the JSON Schema and OpenAPI that applications read.
Every command reads and writes files in `tutorial-work`, so the registry from the first tutorial may be stopped.
The one place a running registry matters is a comparison at the end of the `explain` step,
and that comparison holds whether the registry runs or not.

<QuickstartMeta
  outcome="An edited module re-pinned by content digest, a passing project check, and generated JSON Schema and OpenAPI that carry the changed field."
  time="About 20 minutes"
  level="Local authoring with synthetic data"
  prerequisites={['The binaries and tutorial-work directory from Create and query your first registry', 'An editor', 'Python 3']}
/>

{/* Evidence: crates/registry-bregctl/src/lib.rs, init_files(), project_lock(), and ExplainSubject;
    crates/registry-breg/src/compiler.rs. */}

## Before you start

Open a terminal in the directory that holds `tutorial-work`, and confirm the binaries are still on
your `PATH`:

```sh
bregctl --version
```

The version printed is the one the first tutorial installed.
Nothing in this tutorial touches the registry's keys or data under `tutorial-work/project/.breg/`,
and nothing here needs Docker.

## Open the project

Check the project you generated:

```sh
bregctl check tutorial-work/project
```

```text
Authoring check passed.
  revision  sha256:<digest>

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

This is the project the first tutorial started as a registry, so the entity, fields, and profiles are
the ones you used over HTTP, in the files [init wrote](../first-breg/#what-init-wrote).
The `revision` line is the digest of the compiled project; it changes with every edit you make.

A finding is advice the compiler attaches to a result that succeeded: an error stops a command, a finding does not.
The first says the `operator` profile can list every row, which is intended for a registry-wide operations team,
and the second says the `evidence-source` profile can look up any record by its code.
Every command in this tutorial that succeeds repeats both findings, so treat them as expected and read past them to the result.
A check that is refused prints its errors instead, and you will meet one when you change the module.

{/* Evidence: crates/registry-bregctl/src/lib.rs, init_files() and check(); crates/registry-breg/src/access.rs. */}

## Read the module

A module is a separate file that contributes to the model, so a reusable part of a registry can be reviewed
and versioned apart from the project that adopts it.
Open `tutorial-work/project/modules/record-notes/module.yaml` and find its version and the one field it
declares on the entity the project owns:

```yaml
version: 0.1.0
```

```yaml
      - {id: internal-note, type: string, maxLength: 500, classification: internal}
```

A field without `required: true` is optional, so existing records stay valid and a create may omit it.
Its classification matches the entity's default, `internal`.

Now open `tutorial-work/project/registry.yaml` and find the `modules` entry at the end of the file:

```yaml
modules:
  - id: "record-notes"
    version: "0.1.0"
    digest: "sha256:<digest>"
```

The digest is a content digest of the module file.
The project pins the exact module content it was reviewed with, and the compiler refuses a module whose
content or version no longer matches.

{/* Evidence: crates/registry-breg/src/contract.rs, RegistryModule and EntityExtensionSource;
    crates/registry-bregctl/src/lib.rs, init_files(). */}

## Grant access to the field

Adding a field to the model grants nobody access to it, and no profile lists `internal-note` yet.
In `tutorial-work/project/registry.yaml`, find the `operator` grant for `record` and add the field to its
readable and writable lists, keeping the indentation:

```yaml
readableFields: [code, label, group, status, internal-note]
writableFields: [code, label, group, status, internal-note]
filterableFields: [code, status]
```

Check the project:

```sh
bregctl check tutorial-work/project
```

The command prints `Authoring check passed.`, a new `revision`, and the findings.
`filterableFields` is unchanged on purpose: a note is something an operator reads and writes,
not something a list is filtered by.

## Change the module

Now change the module itself.
In `module.yaml`, raise `maxLength` to `1000` and `version` to `0.2.0`, then run the check again:

```sh
bregctl check tutorial-work/project
```

```text
bregctl check refused.

  error    module.lock.digest_mismatch  project.modules[].digest
           an authored module does not match its locked digest
  error    module.lock.version_mismatch  project.modules[].version
           an authored module does not match its locked version

2 errors, 0 findings.
```

A stale digest is an error, not a finding.
The project still pins version `0.1.0` and the old content, and the compiler refuses to build a model from a
module that differs from what was reviewed.

## Re-pin the module

Rewrite the lock entry from the module's current content, then check again:

```sh
bregctl project lock tutorial-work/project
bregctl check tutorial-work/project
```

`project lock` prints `Locked the project modules. 1 artifact written.`, the rewritten `registry.yaml`, the findings, and a report of what it rewrote:

```json
{
  "changed": true,
  "modules": [
    {
      "digest": "sha256:<digest>",
      "id": "record-notes",
      "status": "updated",
      "version": "0.2.0"
    }
  ]
}
```

`check` then prints `Authoring check passed.` with the findings and nothing else.
Open the `modules` entry in `registry.yaml` again: both the version and the digest changed.
Rerun `project lock` after every module edit, and review a changed digest together with the module source it now pins.

{/* Evidence: crates/registry-bregctl/src/lib.rs, project_lock(); crates/registry-breg/src/compiler.rs. */}

## Inspect the query permissions

`explain` reports what the compiled project permits, without a database:

```sh
bregctl explain queries tutorial-work/project
```

After `Explained the compiled inventory.`, the report lists one operation per profile and entity.
In `records.record.operator.list`, the operator's list operation over `record`, find `internalNote` among the `apiFields`:

```json
{
  "apiName": "internalNote",
  "field": "internal-note",
  "fieldType": {
    "maxLength": 1000,
    "minLength": 0,
    "type": "string"
  },
  "sourceKind": "stored"
}
```

The same operation's `filterable` entries name only `code` and `status`, each with examples such as `$filter=code eq 'example'`.
That is the refusal you met in the first tutorial, explained from the project instead of by a `400`.
This report describes your edited project; if the registry from the first tutorial is still running,
it serves the package it started with and knows nothing of `internalNote`.

{/* Evidence: crates/registry-bregctl/src/lib.rs, ExplainSubject; crates/registry-breg/src/query.rs. */}

## Generate the API schema

Generate JSON Schema and OpenAPI from the edited project:

```sh
bregctl generate schemas tutorial-work/project --output tutorial-work/schemas
bregctl generate openapi tutorial-work/project --output tutorial-work/api
```

Each command names what it wrote: two schema files, then one OpenAPI document.
Read the record schema:

```sh
python3 -m json.tool tutorial-work/schemas/generated/schemas/record.schema.json
```

Find `internalNote` under `properties`.
An optional field accepts `null`, so the generator wraps its type in `anyOf`:

```json
{
  "internalNote": {
    "anyOf": [
      {
        "maxLength": 1000,
        "minLength": 0,
        "type": "string"
      },
      {
        "type": "null"
      }
    ]
  }
}
```

The authored `internal-note` becomes `internalNote` in JSON, and the length you raised in the module reached the schema.
The `required` list still contains only `code` and `label`.
Open `tutorial-work/api/generated/openapi.json` to see the same field in the create and PATCH request
shapes under `/v1/records/records`.
Generated files are outputs: edit YAML and regenerate into a fresh directory to change them,
because `generate` refuses an output directory that already exists.

{/* Evidence: crates/registry-breg/src/artifacts.rs; crates/registry-breg/src/logical_names.rs;
    crates/registry-bregctl/src/lib.rs, generate(). */}

## What you built

You have a project of your own whose module contributes a longer `internal-note`, pinned by content digest,
granted to the operator, and reflected in the JSON Schema and OpenAPI an application would read.
You compiled an extension, not installed one: no database changed, and the registry from the first
tutorial, if it is still running, serves the package it started with.
Testing a project against a database and packaging it come next.

## Troubleshooting

| Symptom | Next move |
| --- | --- |
| `generate` reports `output.destination.invalid` | The output directory must not exist yet. Choose a new path, or remove the one left by an earlier run. |
| `check` reports `module.lock.digest_mismatch` or `module.lock.version_mismatch` | You edited a module after its last lock. Run `bregctl project lock tutorial-work/project`, then check again. |
| `check` cannot parse `registry.yaml` after your edit | Compare the indentation of the lines you changed with their neighbours; the grant lists sit eight spaces in. |
| `bregctl` is not found | Put the installers' directory, `~/.local/bin` unless you changed it, back on `PATH` in this terminal. |
| The running registry does not show `internalNote` | Expected: it serves the package `bregctl dev` built when it first started. Activating a changed package is covered in [Deploy a registry](../../operate/breg/). |
| `bregctl dev` refuses to start the edited project | Expected while the registry holds records: `dev` keeps the package it started with. Run `bregctl dev stop tutorial-work/project --remove` to discard the records, then `bregctl dev tutorial-work/project` builds the edited project into a fresh registry. To keep the records, copy `tutorial-work/project` without its `.breg/` directory to a new path and start that copy instead. |

## Next

- [Review changes before updating a registry](../review-registry-changes/) to test a configurable approval workflow on a copied project.
- [Author a registry project](../../configure/breg/) for the full model: entities, relationships, time, and events.
- [Build a production candidate](../build-a-breg-production-candidate/) to test, package, sign, and verify a project like this one.