Skip to content
Registry StackDocsDevelopment (unreleased)

Modeling patterns for registries

View as Markdown

If you are designing your own Base Registry Engine (BReg) project, start from the questions your registry must answer and the changes callers may make, then choose Record types, fields, and references to fit. BReg has no built-in facility, business, or permit model: every pattern here is ordinary configuration you can read in a starter and adapt.

Terminal window
bregctl init --template facility-registry facility
bregctl check facility

The excerpts below come from the template’s registry.yaml.

Keep a thing separate from its registration when their lifecycles differ. A facility can change operator without becoming a different facility, and a permit’s expiry need not end the business that held it. The template keeps the link in its own Record type, written once and never patched:

- id: facility-assignment
route: facility-assignments
primaryDataset: facility-registry
mutationMode: create-only

Pick the narrowest kind that says what the value is. A string carries a bounded line such as a name or code; text carries long prose. A closed list of states is a vocabulary-code, so a status cannot drift into spelling variants. A date is a full calendar day and a timestamp an instant; decimal takes a declared precision for quantities. When several fields share one shape, declare it once as a value type and refer to it with typeRef:

- {id: postal-code, apiName: postalCode, typeRef: postal-code, required: true, classification: internal}
- {id: status, type: vocabulary-code, vocabulary: facility-status, required: true, classification: internal}

A string or text field can also declare pattern, a PostgreSQL regular expression the database enforces on every write path. It is not anchored for you: [0-9]{13} matches any value containing thirteen digits, so write ^[0-9]{13}$ when you mean the whole value. A pattern checks one row, so a rule that depends on another record belongs in an Action’s commitGuards. Adding a pattern to a running registry checks every existing row first; changing or removing one is a reviewed destructive change. Mark a restricted value encrypted when a database reader must not see it; Field encryption explains what that seals.

The limits on each kind are in the configuration reference.

Keep a scheme identifier, such as a business registration number, separate from the server’s record ID. The issuing scheme decides what the number means and whether it can be reassigned. Use a string for values such as 0017, since a number type loses the leading zeroes.

Use a separate identifier Record type when one holder can have several identifiers, or assignments have their own issuer, validity, or replacement history. Derive uniqueness from the scheme’s rules: two authorities may both issue 0017, while a shared namespace may forbid the duplicate. A unique constraint can span several fields, such as issuer and number, and can apply only when a condition holds. Normalization is not proof that two records describe the same real-world object.

A reference field holds the ID of a record of the named Record type, and the database refuses a value that names no such record. inverseName names the reverse direction, so a facility exposes its assignments without a field of its own:

- {id: facility, type: reference, target: facility, required: true, classification: internal, relationship: {inverseName: assignments}}
- {id: operator, type: reference, target: operator, required: true, classification: internal, relationship: {inverseName: facilityAssignments}}

Use a relationship record whenever the link has information of its own. A read path follows the link in one request; the reader’s access profile still decides what it returns:

readPaths:
- {id: operators, through: facility-assignment, to: operator, route: operators}

The template records the initial assignment and imposes no dated transfer policy. For dated assignments, decide separately when a link applies and which concurrent links your policy allows: one operator, several, or one primary beside other roles. A Record type’s temporal declaration names its start and end fields; exclusivity needs a separate temporal-non-overlap constraint with the right scopeFields. A date or role field alone imposes nothing.

Keep effective time separate from recording time: “who operated the facility on June 1?” differs from “what had the registry recorded on June 1?”. Test adjacent and open-ended periods and a correction to an earlier period.

Generated record routes suit directory maintenance where authorized callers patch each record directly. An immediate Action commits several effects atomically, such as a facility and its first assignment, and a Proposal Action keeps a durable draft for review and later application. Actions and Proposals explains how to choose.

Keep a reported claim separate from the object it describes when the claim has its own reporter, period, or measurement. An inspection and an operator’s declaration can disagree without one overwriting the other. Decide whether a correction patches the claim or adds a successor that references it.

For imported data, retain the source system, source record ID and version, and mapping revision. Record-level provenance cannot explain individual values unless you also keep the source content.

For a document stored elsewhere, keep its identity, immutable version, media type, and digest. A digest compares bytes; it does not identify the issuer. Permission to read that metadata does not grant retrieval of the file. A document submitted with a Proposal uses a bounded attachment slot, not a string field:

attachments:
- {id: supporting-document, required: false, maximumBytes: 1048576, contentTypes: [application/pdf], classification: restricted}

Preserve meaning when values are incomplete

Section titled “Preserve meaning when values are incomplete”

Missing, unknown, not applicable, and withheld are different answers. Use an explicit reason code when the distinction matters, rather than turning absence into zero or false: “no declaration received” differs from “declared quantity is zero”. A known year is not a known January 1; since a date is a full calendar day, less precise information needs a different representation.

Adapt a pattern without inheriting its policy

Section titled “Adapt a pattern without inheriting its policy”

Modules share definitions across projects. An extension adds fields and constraints; it cannot remove an inherited constraint or replace a field, so change the owning definition when its policy does not fit. Review access separately from structure: record reads, Action invocation, and Proposal transitions have separate grants, and a classification label is not an authorization boundary. Test the model with allowed and refused operations, including a case your policy permits that a stricter model might reject.