Released docs. You are viewing the documentation published with v0.34.0. Development docs are available at Latest.
Membership and consent read boundaries
A read grant can require that the caller hold a current active membership in the organization, program, or team responsible for a record. Memberships are ordinary governed records: an authorized steward creates or deactivates them through the normal mutation API, and the reader keeps the same identity token throughout. Base Registry Engine (BReg) compiles the requirement into the database, so the same model works for facility, document, or asset registries without new server code.
A read grant can also require that the subject of a row consented to its disclosure to the caller’s organization, for the request’s purpose, before the row is returned. Consent decisions are ordinary create-only records, and the same database-side model enforces them. Consent-gated reads covers that boundary.
When to reach for one
Section titled “When to reach for one”Reach for a membership boundary when the record itself does not carry the caller’s identity and the
right to read it follows from an affiliation that changes over time. When the record stores the
authorized principal directly, an ordinary rowBoundaries claim comparison is the simpler
mechanism and stays available.
What the permission declares
Section titled “What the permission declares”A facility stores organization, a reference to an organization entity. A separate membership
entity stores an organization reference, a string principal, and a Boolean active. The
boundary joins them inside the facility’s read grant:
permissions: - entity: facility rowBoundaries: [] operations: [get, list, lookup, snapshot, revisions] readableFields: [label] allowCount: true revisionAccess: true membershipBoundaries: - field: organization membershipEntity: membership membershipKeyField: organization principalField: principal activeField: activeThe two organization fields must be stored references to the same entity, principalField must be a
stored string or text field, and activeField a stored Boolean. Only an active-lifecycle membership
row with active: true and an exact match to the profile’s verified principal grants access: a
missing membership, a null value, an inactive membership, or another principal does not. The
declared fields are authorization inputs, not an additional readable-field grant.
Every listed membership boundary and every ordinary rowBoundaries predicate must hold. Declare
rowBoundaries explicitly even when membership is the only row restriction; rowBoundaries: []
adds no direct claim predicate and leaves the membership checks in force. A permission carries at most
eight boundaries, reported as access.membership.limit. Configure the identity claim from a trusted
issuer, and never accept a caller-submitted principal as authority.
What the compiler refuses
Section titled “What the compiler refuses”The refusals keep a membership boundary from being read as broader authority than it is:
| Combination | Diagnostic |
|---|---|
An anonymous profile, or one without a verified principalClaim | access.membership.authentication |
| Writes, actions, reviewed changes, or request lifecycle grants | access.membership.read_only |
| A spatial bbox permission, which uses a separate database authority role | access.membership.spatial_unsupported |
| A membership entity with a change-request lifecycle | access.membership.request_unsupported |
| More than eight boundaries on one grant | access.membership.limit |
A membership-bounded grant supports get, lookup, list, snapshot, and revisions, plus counts when it
sets allowCount. It can protect the root of a relationship read path, and a target permission that
would reach the same entity through a separate root is rejected rather than silently bypassing the
rule. A membership source stays a leaf entity: no boundaries of its own, no change-request
lifecycle, and no incoming relationship read paths, which is what keeps row-security policies from
recursing. A membership entity with mandatory rowBoundaries is rejected at compilation, because
the boundary carries no additional direct row requirement from that source. Use a separate,
explicitly authorized steward profile for membership administration and record mutation, and
bregctl explain access to inspect an expanded boundary beside its scopes, purposes, and ordinary
row boundaries.
Revocation, history, and enforcement
Section titled “Revocation, history, and enforcement”Authorization queries read current membership records in PostgreSQL. A request issued after a deactivation commits is denied or returns an empty authorized collection, including counts, lookups, and cursor continuations; a read already running can finish from the statement snapshot that preceded revocation. There is no token membership cache, external authorization service, or registry-wide authorization lock, and a read cursor replay reevaluates live membership.
Snapshots and revisions compare the retained record’s organization with current memberships, so historical membership state cannot restore access and deactivation removes access to historical records as well. Moving a facility to another organization does not by itself remove access to its older revisions from members of the previous organization: that preserves the ordinary historical row-boundary contract, and current-owner-only history is a different policy the boundary does not imply. Erased history stays unavailable.
PostgreSQL enforces the predicate through generated SECURITY INVOKER functions and forced
row-level security. Each generated PL/pgSQL function saves the prior authorization context, enables
only its matching membership probe, and restores the context before returning, with an exception
block that rolls back the local context on error. It grants no access to private membership records
through direct endpoints or derived views, and it needs no additional database role and no
row-level-security bypass.
Consent-gated reads
Section titled “Consent-gated reads”A membership boundary asks who the caller is affiliated with. A consent boundary asks what the row’s subject decided: whether they agreed that this recipient may read their record for this purpose, under this profile. It is not a lawful-basis engine, a consent-request workflow, or a credential. The registry records what the subject decided and refuses a gated read the recorded decisions do not cover.
Who the recipient is
Section titled “Who the recipient is”A project declares recipients: organizations, each with the OAuth clients it reads through, and
groups of organizations. The caller’s recipient set comes from the verified requester client, the
token’s azp or client_id: the organization that lists that client plus every group it belongs
to. A token cannot supply the set. The engine discards any registry:recipients or
registry:consent-decisions: claim a token carries and fills both from its own configuration, and
a client no organization lists has an empty set, so every consent check it reaches fails closed.
The server refuses to start when a recipient client is missing from the issuer’s allowedClients.
When a row is disclosed
Section titled “When a row is disclosed”A permission lists requireConsent checks, each naming a consent record and the row key it
matches: the row’s own id, or a stored reference to the subject. A row is disclosed while, for
every check, a live give exists whose subject is the row’s key, whose scope is the gated profile
id, whose purpose is the request’s verified purpose, and whose recipient is in the caller’s set.
Decisions are ordered by their capped time, the earlier of the declared start and the moment the
registry recorded them. A withdrawal whose capped time is at or after a give’s ends that give, and
on a tie the withdrawal wins. So a future-dated give cannot outrank a withdrawal recorded after it,
and a backdated give recorded after a withdrawal is superseded by it: a renewed give needs a
current start.
A give ends at the earlier of its until and its capped time plus the record’s maxDuration.
Expiry is computed at read time, and a withdrawal takes effect for transactions that start after it
commits, with no token change, cache flush, or restart.
The profile id is the scope a subject agreed to. Removing requireConsent or widening a gated
profile in place would read under a scope the subject never saw, so the supported change is a new
profile id with the old one listed under retiredConsentScopes. bregctl diff marks the in-place
change for review and attaches a reason; that reason is a review aid in tooling output, not a
runtime guarantee. Consent codes are append-only: stored decisions keep their recipient and scope
codes, so a recipient is retired by removing its clients, not its entry.
What the compiler refuses
Section titled “What the compiler refuses”| Combination | Diagnostic |
|---|---|
| Writes, actions, reviewed changes, or request lifecycle grants on a gated permission | consent.require.read_only |
| An anonymous gated profile | consent.require.anonymous |
A gated profile without requesterClients, or with a client no recipient organization lists | consent.require.clients |
| A spatial bbox permission, which uses a separate database authority role | consent.require.spatial_unsupported |
| Bulk data export of a gated permission | consent.require.export_unsupported |
| Exporting a gated profile as an Evidence source | consent.require.evidence_source_unsupported |
| A read path that reaches a gated entity through a later step | consent.require.read_path_target |
A profile granting create or batch on a consent record, which only actions declaring consentIssuer write | consent.record.direct_write |
| A consent record no permission requires yet | consent.require.unused |
Every read operation of a gated permission is checked: get, lookup, list, count, snapshot,
revisions, continuation cursors, and read paths rooted at the gated entity. A row without consent
answers 404 exactly like an absent row.
What it does not enforce
Section titled “What it does not enforce”A profile without requireConsent bypasses consent. bregctl check reports
access.consent.ungated_client when an ungated profile reads a gated entity while it admits any
client, or a client a gated profile admits; give each side its own clients. A recipient reads the
decisions addressed to it through a feed profile, and that feed shows withdrawals and
invalidations addressed to it, including one for a subject it never received a give for; refusals
never reach it. Assisted capture and link provisioning rest on staff integrity and the audit
journal. List response timing is not equalized, so a caller measuring many requests may infer the
size of a gated population. Consent gates rows, not fields.
How PostgreSQL enforces it
Section titled “How PostgreSQL enforces it”Each consent record compiles to one SECURITY INVOKER set-returning probe function, evaluated
once per query under the same processing-only marker model as membership boundaries. The runtime
sets the caller’s recipients in the record transaction from the verified client, never from a
token claim or request header, and the probe matches nothing when that setting is missing or
malformed. The compiler also creates the indexes the probe reads, and a runtime refuses a probe
whose catalog definition was altered.
- Control access per profile to author the profile, its scopes, and the permission the boundary sits in, including a consent-gated read.
- Base Registry Engine configuration reference for every permission key a fragment names.
- Modeling patterns for registries for the record structures a membership model joins.
- Current membership access for the full contract and its fixture project.
- Consent-gated reads for the full consent contract, its generated module, and its fixture projects.