Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.44.0.
If you operate Registry Casework, deploy a reviewed policy package against PostgreSQL, verify its activation, then establish the teams that can see and decide its review tasks. Casework returns a review result; the producer keeps all authority to change its subject.
What you provide
Section titled “What you provide”- PostgreSQL 17 or newer, with TLS and separate migration and runtime credentials.
- An OpenID Connect issuer, the intended audience, and an explicit list of admitted clients.
- A private HTTP listener behind a TLS proxy you operate.
- A reviewed policy package, the matching binaries, and secret files the process can read.
- A durable audit destination and an archive for it.
- A directory team serving each queue reviewers work.
Review and directory state live in PostgreSQL; the audit journal lives outside it. A current-source policy also needs a read-only context provider.
Connect identity and completion delivery
Section titled “Connect identity and completion delivery”Establish issuer signing-key trust and TLS trust separately. The
OIDC connection guide explains issuer configuration and
key discovery; the Casework runtime’s authentication.oidc block binds the accepted resource,
clients, and human identity claim. Configure downstream HTTPS trust through the owning connection’s
certificate setting, rather than weakening verification.
Present a resource-specific access token and select the authored profile with
Registry-Casework-Profile on Casework task and directory routes. Shared producer Review routes
use Registry-Review-Profile. The Casework API owns the
route, token, scope, and profile requirements; a scope does not replace an authored access profile.
For a producer completion destination, follow
completion destinations, then read the result
through the producer’s independently authorized client. The wake carries no decision and cannot
approve or apply a source operation. Acknowledge only after accepting the wake under the
delivery contract; authentication,
binding, and acknowledgement failures must remain visible to the dispatcher. For released 0.43 behavior, return exactly
204 No Content with an empty body after acceptance. The shared delivery contract governs the
next release’s acknowledgement behavior; returning an empty 204 remains compatible with both.
Install the runtime
Section titled “Install the runtime”Install matching casework and caseworkctl binaries on the serving and operator hosts:
curl -fsSL https://github.com/registrystack/registry-stack/releases/latest/download/casework-install.sh | bashcasework --versioncaseworkctl --versionRead the installer before running it. Its SHA256SUMS check does not establish release
authenticity; use the
release verification procedure
for that.
The container image carries both binaries. Its entrypoint serves Casework from
/etc/registry-casework/runtime.yaml; run operator commands with the entrypoint set to
/usr/local/bin/caseworkctl. Pin the image digest, mount the package, runtime file, and secret
root read-only, and give the audit directory durable writable storage.
Provision PostgreSQL
Section titled “Provision PostgreSQL”Use a new database. As your PostgreSQL administrator, create a schema-owning migration role, a separate runtime role, and the database:
CREATE ROLE casework_migration LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS PASSWORD '<migration-password>';CREATE ROLE casework_runtime LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS PASSWORD '<runtime-password>';CREATE DATABASE casework;In that database, grant connection access and give the migration role the schema:
REVOKE ALL ON DATABASE casework FROM PUBLIC;GRANT CONNECT ON DATABASE casework TO casework_migration, casework_runtime;ALTER SCHEMA public OWNER TO casework_migration;REVOKE ALL ON SCHEMA public FROM PUBLIC;Only caseworkctl apply uses the migration credential, so keep it off the serving host. Apply
grants the runtime role what it serves with and keeps the activation ledger read-only to it.
If apply reports casework.activation.role-mode-weakened, make the ownership, privilege, or
trigger correction it names, then plan again; never give the runtime role ledger writes to get past
it. When both credentials name one role, the deployment records single-role mode, and status
says the runtime credential can activate packages.
Write the runtime file
Section titled “Write the runtime file”The runtime file is yours, kept apart from the authored policy. This one uses mounted secrets and a TLS proxy in front:
apiVersion: id.registrystack.org/formats/casework/runtime/v1alpha1kind: CaseworkRuntimeConfigidentity: databaseId: casework-decisionspackage: root: /srv/registry-casework/packagelistener: bind: 10.42.0.7:8100 tlsTermination: operator-controlled-upstream networkExposure: private-addresssecretProviders: file: root: /run/secrets/registry-caseworkdatabase: runtimeUrlRef: secret:file/runtime-database-url migrationUrlRef: secret:file/migration-database-urlauthentication: oidc: issuer: https://identity.example.test/realms/registry audience: urn:example:casework allowedClients: [casework-console, review-producer] scopeClaim: scope humanIdentity: claim: registry_actor_kind value: humanaudit: path: /var/lib/registry-casework/audit/casework.ndjson hashKeyRef: secret:file/casework-audit-keyEvery path is absolute, and the runtime file may not pass through a symbolic link. Unknown keys and
explicit nulls are refused at their position; omit a key to take its default.
identity.databaseId is your stable name for this deployment: the first apply records it, and
every later apply and start refuses a database that recorded another.
Each ...Ref key names a secret, never its value: secret:file/<name> resolves under
secretProviders.file.root. Mount each secret as a file owned by the runtime user, mode 0400 or
0600, with no symbolic or extra hard link. operator-controlled-upstream requires
allowedClients to list its clients. Ship the audit journal to your archive;
Review retention and audit covers its key and
rotation.
Add a current-source provider
Section titled “Add a current-source provider”A current-source policy names a provider by its deploymentRef. Bind it to a read-only provider:
contextProviders: facility-registry: baseUrl: https://registry.example.test statusProfile: casework-review-status tokenEndpoint: https://identity.example.test/oauth2/token clientAssertionAudience: https://identity.example.test resource: urn:breg:facility-registry scopes: [registry:proposal:review-status] clientIdRef: secret:file/breg-status-client-id clientAssertionKeyRef: secret:file/breg-status-client-keyThis Base Registry Engine (BReg) status reader checks reviewability only; its statusProfile
must match the authored service-status profile. Human display uses the reviewer’s own bearer, so
source authorization and disclosure still apply. A Casework approval never applies a BReg
Proposal.
Check the runtime file offline
Section titled “Check the runtime file offline”Check the authored project against the runtime file before anything touches a database:
caseworkctl check ./decisions --runtime-config /etc/registry-casework/runtime.yamlThe check opens no package, database, issuer, provider, or secret. Add --environment to fill
${VAR} expressions from the current environment. A clean check does not prove live readiness.
runtime.yamlSource preview, pre-1.0, 77 key paths
runtime.yaml
Source preview, pre-1.0
Generated from products/casework/generated/runtime/runtime.schema.json. 77 key paths. Explained in docs/site/src/content/docs/operate/casework.mdx.
| Key path | Type | Required | Accepted values | Constraints |
|---|---|---|---|---|
apiVersion | string | Yes |
| None specified |
auditThe audit journal: where it is written and the key its hashes use. | object | Yes | Not specified | None specified |
audit.destinationWhere audit entries go: a rotated file (the default) or stdout. | string | No |
"file" | None specified |
audit.hashKeyRefSecret reference to the audit hash key. | string | Yes | Not specified |
|
audit.pathThe active audit file. Required for, and only allowed with, file. | string | Conditional | Not specified |
|
audit.retentionDaysDelete rotated files older than this many days. Only allowed with
file; omitted, rotated files are kept 90 days. | integer | No | Not specified |
|
audit.rotateBytesRotate the active file once it reaches this many bytes. Only allowed
with file; omitted, the file rotates at 100 MiB. | integer | No | Not specified |
|
authentication | object | Yes | Not specified | None specified |
authentication.oidcThe access tokens this runtime accepts: the issuer and its keys, the
clients admitted, the scope claim, and the claim that marks a human actor. | object | Yes | Not specified | None specified |
authentication.oidc.allowInsecureHttpPermit plain HTTP beyond loopback to the configured issuer and to a
JWKS URI declared in configuration. TLS remains required unless the
operator explicitly accepts this transport risk. | boolean | No | Not specifiedSchema default, where declared: false | None specified |
authentication.oidc.allowedClientsThe OAuth clients whose access tokens are admitted: unrestricted to
admit a token from every client the issuer verifies, or a list of at
least one distinct client. A runtime decides whether unrestricted
is acceptable in production. | array | string | Yes |
| No bound or
|
authentication.oidc.allowedClients[]Array item | string | Conditional | Not specified | None specified |
authentication.oidc.assertionIssuersAssertion authorities each client may exchange a subject token from,
keyed by client identifier. Omitted, no assertion-issuer rule applies;
written, it lists at least one client, each with at least one
authority. Once a client is listed, a token it exchanged is accepted
only for one of that client's declared authorities, and a client that
is not listed may exchange from no authority. | object | No | Not specified |
|
authentication.oidc.assertionIssuers.*Map value | array | No | Not specified |
|
authentication.oidc.assertionIssuers.*[]Array item | string | No | Not specified |
|
authentication.oidc.audienceThe audience every accepted access token must carry in aud. | string | Yes | Not specified |
|
authentication.oidc.humanIdentity | object | No | Not specified | None specified |
authentication.oidc.humanIdentity.claim | string | No | Not specifiedSchema default, where declared: "registry_actor_kind" | None specified |
authentication.oidc.humanIdentity.value | string | No | Not specifiedSchema default, where declared: "human" | None specified |
authentication.oidc.issuerExact issuer accepted in access-token iss claims, an absolute
https URL, or a plain http URL when allowInsecureHttp is set. | string | Yes | Not specified |
|
authentication.oidc.jwksSourceWhere the issuer's signing keys come from. Absent reads the issuer's
OpenID Connect discovery document. | object | No | Not specifiedSchema default, where declared: {"type":"discovery"} | None specified |
authentication.oidc.jwksSource.documentRefAn exact secret reference: secret:file/name, resolved under secretProviders.file.root, or secret:env/NAME, resolved only when secretProviders.environment is declared. | string | Conditional | Not specified |
|
authentication.oidc.jwksSource.type | string | Yes |
| None specified |
authentication.oidc.jwksSource.uriAn absolute http or https URL with a host preserved as written, no user information, and no backslash, whitespace or control character. DNS case and equivalent IPv6 literals are permitted. | string | Conditional | Not specified |
|
authentication.oidc.maximumTokenLifetimeSecondsOptional deployment bound on the lifetime declared by an incoming
access token, independently of the issuer's own policy. | integer | No | Not specified |
|
authentication.oidc.revokedKeyIdsSigning-key identifiers Casework refuses locally, even while the issuer
still publishes them; listing a key does not revoke it at the issuer.
Omitted, no key is refused; written, the list is non-empty. | array | No | Not specified |
|
authentication.oidc.revokedKeyIds[]Array itemAn identifier another system issues, written exactly as its issuer gives it. | string | No | Not specified |
|
authentication.oidc.scopeClaim | string | No | Not specifiedSchema default, where declared: "registry_scopes" | None specified |
contextProvidersProvider transport bindings keyed by the deploymentRef casework.yaml declares. | object | No | Not specifiedSchema default, where declared: {} | None specified |
contextProviders.*Map valueEnvironment-specific transport and reader identity. Human display always replaces it. | object | No | Not specified |
|
contextProviders.*.baseUrlAn absolute http or https URL with a host preserved as written, no user information, and no backslash, whitespace or control character. DNS case and equivalent IPv6 literals are permitted. | string | Yes | Not specified |
|
contextProviders.*.clientAssertionAudience | string | No | Not specified | None specified |
contextProviders.*.clientAssertionKeyRefAn exact secret reference: secret:file/name, resolved under secretProviders.file.root, or secret:env/NAME, resolved only when secretProviders.environment is declared. | string | Yes | Not specified |
|
contextProviders.*.clientIdRefAn exact secret reference: secret:file/name, resolved under secretProviders.file.root, or secret:env/NAME, resolved only when secretProviders.environment is declared. | string | Yes | Not specified |
|
contextProviders.*.resource | string | No | Not specified | None specified |
contextProviders.*.scopes | array | No | Not specified | None specified |
contextProviders.*.scopes[]Array item | string | No | Not specified | None specified |
contextProviders.*.statusProfile | string | Yes | Not specified | None specified |
contextProviders.*.taskGrantProfile | string | No | Not specified | None specified |
contextProviders.*.tokenEndpointAn absolute http or https URL with a host preserved as written, no user information, and no backslash, whitespace or control character. DNS case and equivalent IPv6 literals are permitted. | string | Yes | Not specified |
|
databaseThe PostgreSQL connection a stateful runtime uses. Both URLs are secret
references and may name the same secret. | object | Yes | Not specified | None specified |
database.migrationUrlRefSecret reference to the migration connection URL. | string | Yes | Not specified |
|
database.runtimeUrlRefSecret reference to the least-privileged runtime connection URL. | string | Yes | Not specified |
|
database.testOnlyPlaintextAllow a plaintext connection. Refused outside test builds. | boolean | No | Not specifiedSchema default, where declared: false | None specified |
database.trustedRootCertificateRefSecret reference to a PEM root certificate the connection trusts.
Absent, the connection trusts the platform's root certificates. | string | No | Not specified |
|
identityThe logical identity of the database this deployment activates packages
in. It is chosen by the operator (for example casework-production) and
never derived from a URL or a PostgreSQL database name. The first
caseworkctl apply records it, and every later apply and every startup
refuses a database whose recorded identity differs. | object | Yes | Not specified | None specified |
identity.databaseId | string | Yes | Not specified |
|
kind | string | Yes |
| None specified |
listenerThe listener of a runtime that declares its TLS termination and network
exposure. | object | Yes | Not specified | None specified |
listener.bindSocket address the runtime listens on, written host:port with an IP address host ([addr]:port for IPv6). | string | Yes | Not specified |
|
listener.networkExposureThe operator-declared private network placement of an HTTP listener. | string | No |
"private-address" | None specified |
listener.tlsTerminationDeclares the trusted transport boundary for a runtime's plaintext HTTP
listener. Production listeners require operator-controlled upstream TLS
termination; direct plaintext is limited to the explicit loopback-only
development mode. | string | Yes |
| None specified |
metricsListenerOperator-private listener for /metrics and /version.
It is separate from the API listener so the counters and the active
package digest never appear on the surface the public contract describes. | object | No | Not specified | None specified |
metricsListener.bind | string | Yes | Not specified | None specified |
package | object | Yes | Not specified | None specified |
package.expectedDigestsha256: label of the package digest, the digest of the package's
SHA256SUMS file. When set, the runtime refuses to start on any other
package, and on an authored project that has no SHA256SUMS.
Omitted, the runtime starts on whichever package root holds. | string | No | Not specified |
|
package.rootAbsolute path of the package directory. | string | Yes | Not specified |
|
reviewCompletionDestinationsCompletion destinations keyed by the destination id a review producer
names in casework.yaml, kept as written. | object | No | Not specified | None specified |
reviewCompletionDestinations.*Map value | object | No | Not specified |
|
reviewCompletionDestinations.*.attemptTimeoutMilliseconds | integer | No | Not specifiedSchema default, where declared: 5000 |
|
reviewCompletionDestinations.*.authHow a completion destination presents its secret. Without header the
secret is sent as Authorization: Bearer; with one, the raw secret is the
value of that header and no Authorization header is sent. | object | Conditional | Not specified | None specified |
reviewCompletionDestinations.*.auth.header | string | No | Not specified |
|
reviewCompletionDestinations.*.auth.secretRefAn exact secret reference: secret:file/name, resolved under secretProviders.file.root, or secret:env/NAME, resolved only when secretProviders.environment is declared. | string | Yes | Not specified |
|
reviewCompletionDestinations.*.bearerTokenRefShorthand for auth with no header: the secret is presented as
Authorization: Bearer. Exactly one of this and auth is configured. | string | Conditional | Not specified |
|
reviewCompletionDestinations.*.maximumAttempts | integer | No | Not specifiedSchema default, where declared: 12 |
|
reviewCompletionDestinations.*.retryDelaySeconds | integer | No | Not specifiedSchema default, where declared: 30 |
|
reviewCompletionDestinations.*.urlAn absolute http or https URL with a host preserved as written, no user information, and no backslash, whitespace or control character. DNS case and equivalent IPv6 literals are permitted. | string | Yes | Not specified |
|
secretProvidersThe secret providers a runtime enables. A reference is resolved only by a
provider declared here: secret:file/name under file.root, and
secret:env/NAME only when environment: {} is present. | object | Yes | Not specified | None specified |
secretProviders.environmentEnables secret:env/NAME references, read from the process
environment. Declared as an empty mapping: environment: {}. | object | Conditional | Not specified | None specified |
secretProviders.fileEnables secret:file/name references, read from files under root. | object | Conditional | Not specified | None specified |
secretProviders.file.rootAbsolute directory holding one file per secret. Each file must be a
regular file owned by the runtime user, mode 0400 or 0600, with exactly
one hard link. | string | Conditional | Not specified |
|
taskAuthority | object | No | Not specified | None specified |
taskAuthority.exchangeAudience | string | Yes | Not specified | None specified |
taskAuthority.issuerAn absolute http or https URL with a host preserved as written, no user information, and no backslash, whitespace or control character. DNS case and equivalent IPv6 literals are permitted. | string | Yes | Not specified |
|
taskAuthority.signingKeyRefAn exact secret reference: secret:file/name, resolved under secretProviders.file.root, or secret:env/NAME, resolved only when secretProviders.environment is declared. | string | Yes | Not specified |
|
taskAuthority.statusClientsService client IDs mapped to their one protected resource audience. | object | Yes | Not specified | None specified |
taskAuthority.statusClients.*Map value | string | No | Not specified |
|
Verify the package before it opens the listener
Section titled “Verify the package before it opens the listener”Write the package with caseworkctl package, install it in its own directory, and set
package.root to it. Record its packageDigest in your change record and set
package.expectedDigest to the same value, so that replacing files under package.root cannot
change what a restart loads. Never edit a package in place: apply and startup both verify every
file against SHA256SUMS, and an authored directory without one is refused.
Keep earlier package directories for recovery, and plan every activation, rollbacks included.
Refuse a package that strands pinned work
Section titled “Refuse a package that strands pinned work”An accepted review keeps the policy closure it was admitted under. A candidate that removes a
policy, producer, profile, team, queue, provider, or context contract an open review still needs is
refused with casework.activation.stranded-work, and so is a change to pinned policy content under
the same policy version. There is no override. Let the affected reviews finish, have their producer
cancel them, or prepare a successor that keeps what they need.
Plan, apply, and serve
Section titled “Plan, apply, and serve”From the operator host, read the plan, apply it with the migration credential, and read the result:
caseworkctl plan --runtime-config /etc/registry-casework/runtime.yamlcaseworkctl apply --runtime-config /etc/registry-casework/runtime.yaml \ --operator-reference CHANGE-1234 --backup-reference nightly-before-activationcaseworkctl status --runtime-config /etc/registry-casework/runtime.yamlplan writes nothing; read its package digest, pending schema versions, role mode, and refusals.
Applying the package that is already active is refused with
casework.activation.already-active. --operator-reference is stored as a keyed hash;
--backup-reference is stored as text, so use a snapshot identifier without personal data.
If apply returns casework.activation.applied-unaudited, the activation committed but its audit
entry did not: restore audit, read status, and record the recovery. A runtime role that cannot
read the ledger gets casework.activation.ledger-unreadable; apply restores its grants. Exit codes
are 0 for success, 1 for a refusal, 2 for usage, and 3 for an operational failure.
Then serve as the runtime user:
casework --runtime-config /etc/registry-casework/runtime.yaml servecurl --fail http://10.42.0.7:8100/readyStartup refuses a database whose active package is not the configured one. /ready checks the
database and audit writer only; check source context and queue staffing with real identities.
Earlier database and audit state
Section titled “Earlier database and audit state”The current contract refuses retired Casework schemas with casework.activation.retired-schema,
a database a pre-1.0 release wrote with casework.activation.beta-schema, and a database newer
than the binary with casework.activation.schema-newer. Keep such a database with the release
that wrote it and provision a fresh one; for a pre-1.0 database, follow
the upgrade runbook. Never delete ledger rows or run migration
SQL by hand to get past either refusal.
A restore returns review state to its backup point, and the external audit journal must not be rewound. Follow Casework restore and reconciliation before reviewers resume.
Scrape metrics and the running version
Section titled “Scrape metrics and the running version”The API listener serves no metrics. Open a private telemetry listener for them:
metricsListener: bind: 127.0.0.1:9100It serves /metrics and /version only, on a loopback or private address that is not the API
listener’s. Metrics count open review requests, open tasks, and due tasks, with no subject
identifiers. /version reports the binary version and the package digest the process serves, so a
rollout can confirm what each process runs.
Establish directory authority
Section titled “Establish directory authority”A fresh deployment has an empty directory, so no reviewer sees a task until an administrator bootstraps the first team and its served queues. Staff the Casework directory gives the requests. Each directory write names the revision it expects, and membership and queue service are live authority for old and new tasks alike.
Size the review queue reads
Section titled “Size the review queue reads”The inbox policy in casework.yaml bounds each task-list, next-task, and holdings read. Defaults
are maximumCandidateScan: 100, maximumSourceReads: 25, and pageDeadlineMilliseconds: 2000;
counts go up to 10,000 and the deadline to 30,000. An ordinary due-date or subject-order query
selects a bounded database page before it reads current source visibility, so it never scans the
whole queue; concealed candidates still spend the scan budget. Filtering or sorting by live source
reference scans the whole bounded queue, because its keys come from source reads; an exact subject
or queue selector narrows it, a smaller page does not.
Within one page, several task slots for one request share one source observation. Each new page repeats the source and Directory checks, and a continuation charges its anchor observation to the same source budget, so a budget of one observation cannot advance past a current-source anchor. A BReg human context reads each chunk in one bounded HTTP batch under the original caller and source profile; every subject in it still passes its own disclosure and audit checks.
An ordinary read that exhausts its candidate, source, or time budget is refused whole with
review.query-too-broad: narrow the query or reduce the page limit. A source call that reaches the
deadline can instead report the source unavailable. Supervision and own-decision feeds return
bounded partial pages with opaque continuations; follow the cursor with the same caller, profile,
and query, and start the walk again after the cursor expires or its anchor leaves your scope.
Measure representative source latency before raising budgets. A caseload preview refuses more than
100 claimed tasks; split it by queue or move tasks one at a time.
Check readiness with doctor
Section titled “Check readiness with doctor”Run the operator preflight as the runtime user:
caseworkctl doctor --runtime-config /etc/registry-casework/runtime.yamlDoctor checks the configuration, the package, the database activation, open reviews against the
configured package, and that the audit path is writable. It accepts the package already active.
It warns when the serving teams cannot supply the distinct reviewers a stage requires; add eligible
members and rerun it to confirm the warning clears. It opens no listener and proves no issuer or
provider access; use /ready and an authenticated task read for those.
Change the active package
Section titled “Change the active package”A running process keeps the package it started with and does not recheck the activation ledger,
so a predecessor left serving after apply goes on admitting work under the old package. Take it out
of your proxy’s routing, stop every process serving it, then plan, apply, and start the successor
at once. The successor repeats the pinned-work check before it opens its listener. If it refuses
with casework.activation.stranded-work, plan and apply the predecessor again, serve it until the
named reviews finish or are cancelled, and retry the change.
Change an active package walks the plan and apply against a local
session.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Recovery |
|---|---|
platform.runtime-config.path | Give the absolute path of the runtime file, without . or .. components. |
| Startup names a different active package | Plan and apply the configured package, or point package.root at the active one. |
review.policy-mismatch on admission | Reconcile the producer’s policy pin with the deployed policy. |
| A current-source task is concealed or unavailable | Check the reviewer’s source profile, the context binding, and the provider. Never use the status credential for human display. |
review.query-too-broad | Narrow the queue or subject, reduce the page limit, review the inbox budgets, or split a caseload move. |
precondition.failed | Read the task or directory again and take a fresh preview where one is required. |
service.unavailable and /ready returns 503 | Repair the database or the audit path, space, or permissions and restart. Retry an interrupted write with its original key. |
Cookies on Registry Stack
We use optional cookies to understand how you use the main Registry Stack website and this documentation. If you contact us, we may link this activity to your enquiry. You can change your choice at any time. Read the privacy policy.
Your browser asks sites not to track you, so optional tracking stays off.
We could not use or save that choice. Optional tracking remains off.