Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.26.1.
An author has handed you a reviewed Registry Casework policy package and you want it serving. At
the end of this page a casework process serves that package against PostgreSQL behind your own
TLS proxy, answers GET /ready, and has a directory whose teams serve every queue the package
declares.
Authoring, packaging, and the policy inside the package stay with the author; authoring a Casework policy covers that side. This page starts from the package directory and ends at a serving deployment.
What you provide
Section titled “What you provide”- One PostgreSQL database and two login credentials. The migration credential owns the Casework schema and applies migrations. The runtime credential serves requests and the background workers. Casework creates no extension and no role, and its migrations carry no version-gated SQL; the product’s own database suites run against PostgreSQL 17. The runtime requires TLS on both connections and refuses a plaintext one.
- One OpenID Connect issuer. Casework verifies bearer access tokens and issues none. Configure
the issuer to add the exact human-identity assertion,
registry_actor_kind: humanby default, only to interactive human sessions, and to issue the Requester scope to the services that call on a person’s behalf. A Staff, Supervisor, or Administrator token without that assertion is refused even when it carries valid scopes and names a directory member. - A TLS proxy or ingress in front of the listener. The process serves plain HTTP on a private or container-private address and reads no forwarded header, so TLS termination, HSTS, and client addresses stay with your proxy.
- Secrets the process can read. Every credential in the runtime file is a reference:
secret:file/<name>names an owner-only file under one file provider root, andsecret:env/<NAME>names an environment variable. - Durable storage for the audit file. The runtime appends a keyed hash chain to one JSONL file and holds a process-lifetime lock on a sentinel file beside it, so exactly one process writes one chain.
- A host for one process, or a place to run the container image.
What it does not need
Section titled “What it does not need”A hosted deployment that declares no source needs no Base Registry Engine (BReg): the work items its callers create live in Casework’s own database, and the source bindings in the runtime file exist only for the sources a package declares. Work, claims, drafts, attempts, accountability records, and the idempotency ledger all live in PostgreSQL, so no message broker and no cache take part in a request. Browser traffic reaches your own application host, which calls Casework server to server, so the API enables no CORS.
Install the runtime
Section titled “Install the runtime”Install the binaries on the host that will serve, and on the operator host that holds the migration credential:
curl -fsSL https://github.com/registrystack/registry-stack/releases/latest/download/casework-install.sh | bashcasework --versioncaseworkctl --versionThe installer verifies every downloaded binary against the release SHA256SUMS before anything
reaches the install directory, and installs casework, caseworkctl, and mint together or not
at all. Registry Mint comes along because a deployment with no identity provider uses it to issue
access tokens; a deployment with its own OpenID Connect issuer can ignore it. The installer does
not verify release authenticity. Replace | bash with | less to read it before running it on a
host you operate. CASEWORK_INSTALL_DIR selects the install directory, and the default is
~/.local/bin; CASEWORK_VERSION pins one release; CASEWORK_ASSET_DIR installs from a directory
you verified yourself. Binaries are published for linux-amd64, linux-arm64, and macos-arm64,
and the release also publishes casework-install.sh as a movable alias of the pinned installer.
The container image is ghcr.io/registrystack/casework:v0.30.0, built on a distroless nonroot base
for linux/amd64. Its entrypoint is /usr/local/bin/casework, its default arguments are
--runtime-config /etc/registry-casework/runtime.yaml serve, and it exposes port 8100. It carries the
runtime binary and its license and nothing else: no shell, no healthcheck command, and no
caseworkctl, so migration and doctor run from a separate operator host. One directory is
writable, /var/lib/registry-casework/audit, owned by UID and GID 65532 with mode 0700, for the
audit chain; mount the runtime file, the policy package, and the secret root read-only. The
release manifest published beside the binaries records the promoted digest of the image; pin that
digest rather than the movable tag.
Write the runtime file
Section titled “Write the runtime file”The runtime file binds one policy package to one database, one issuer, one listener, and one binding per source the package declares. It is a deployment artifact: keep it outside the authoring project, and put no credential in it.
apiVersion: registry.registrystack.org/casework-runtime/v1alpha1kind: CaseworkRuntimeConfigpackage: # The selected package always contains the policy at casework.yaml. root: /etc/registry-casework/packagelistener: # The proxy in front of this private listener terminates TLS. bind: 10.42.0.7:8100 # operator-controlled-upstream for production; development-loopback only for loopback. tlsTermination: operator-controlled-upstream # container-private also accepts a wildcard bind on a private container network. networkExposure: private-addresssecretProviders: file: # Owner-only files, one per reference. root: /etc/registry-casework/secretsdatabase: runtimeUrlRef: secret:file/casework-runtime-database-url migrationUrlRef: secret:file/casework-migration-database-url # Optional: a private certificate authority for the PostgreSQL connection. trustedRootCertificateRef: secret:file/casework-database-root.pemauthentication: oidc: issuer: https://identity.example.org/realms/registry audience: urn:example:casework scopeClaim: scope humanIdentity: claim: registry_actor_kind value: human jwksSource: kind: discoveryaudit: path: /var/lib/registry-casework/audit/casework.ndjson hashKeyRef: secret:file/casework-audit-keysources: # One entry per source the package declares, keyed by its exact source id. professional-register: baseUrl: https://registry.example.org readerProfile: casework-reader tokenEndpoint: https://identity.example.org/realms/registry/token clientIdRef: secret:file/breg-reader-client-id clientAssertionKeyRef: secret:file/breg-reader-key webhookSecretRef: secret:file/breg-casework-webhook eventSource: urn:registrystack:registry:professional-licences:instance:professional-licences-1| Section | What it binds |
|---|---|
package | The absolute root of the package containing casework.yaml, its manifest, and exact source descriptions. |
listener | The listener address and the transport boundary the deployment declares for it. |
secretProviders | The explicitly enabled file and environment secret providers. |
database | Secret references for the runtime and migration connection URLs, and an optional trusted root certificate for the PostgreSQL connection. |
authentication | The OpenID Connect issuer, audience, claim names, human-identity assertion, and the source of the issuer’s keys. |
audit | The path of the external JSONL chain and the reference to the key that chains it. |
sources | One BReg binding per source the package declares: base URL, reader profile, token endpoint, and references to the client identifier, client assertion key, and webhook secret. The set of keys must equal the set of declared source ids exactly, and a package with no source needs no block. |
The package root, file secret root, and audit path must be absolute. An environment reference is
valid only when secretProviders.environment: {} explicitly enables that provider.
The listener boundary is checked, not advisory. With operator-controlled-upstream and
private-address, the address must be loopback or private (IPv4 private range or IPv6 unique
local); container-private also accepts a wildcard bind for a container on a private network.
development-loopback accepts only a loopback address with private-address, and it is the one
mode that serves an authored project with no manifest beside it. Apply HSTS on the proxy’s TLS
responses; the runtime adds its remaining security headers and Cache-Control: no-store to every
response it returns.
scopeClaim defaults to registry_scopes for compatibility with earlier deployments. When the
issuer is Registry Mint, set scopeClaim: scope explicitly so its OAuth scope values carry every
required scope for the selected profile. humanIdentity defaults to the claim
registry_actor_kind with the value human, must
differ from scopeClaim, and no access profile in the package may use it as its principal claim.
Each access profile names its own principalClaim; there is no runtime-wide principal claim.
The runtime accepts RS256 and ES256 signatures and the token types at+jwt and JWT.
With the discovery source, the runtime reads the issuer’s discovery document at startup and
fetches its keys from the jwks_uri that document names; an optional jwksUri in this file
overrides that one value and leaves the rest of discovery in place. Prefer the static alternative
when the Casework host cannot reach discovery, or when you pin the issuer’s keys deliberately:
jwksSource: kind: static documentRef: secret:file/casework-issuer-jwksThe document must be a JWKS with at least one RSA or elliptic-curve key, each carrying a distinct
non-empty kid. It is read once, at startup, so a key rotation at the issuer is a replacement of
that secret and a restart of every serving process.
Each source binding also accepts eventType, which defaults to casework-lifecycle-v1,
trustedRootCertificatesRef for a private certificate authority in front of the source,
requestTimeoutMilliseconds and connectTimeoutMilliseconds, which default to 30000 and 10000,
accept at most 300000, and are refused when the connect timeout is the larger of the two, and
reconciliationIntervalMilliseconds, how often the runtime re-reads the source for work the
source’s own notifications did not deliver, which defaults to 60000 and accepts 1000 to 3600000.
The interval changes scheduling only. Changing it does not invalidate displayed source bindings or
durable attempts because it does not change source authority or saved source state. If one pass
outlasts the interval, Casework skips missed ticks instead of replaying them back-to-back against the
source.
A pinned document keeps accepting the keys it holds and refusing every other one, so an issuer rotation that nobody mirrors into the secret refuses fresh logins. Replace the secret and restart the process as part of the rotation, and confirm the replacement keys with the issuer’s operator before they become the anchor for verification.
Create the secret files
Section titled “Create the secret files”Create each file as the user that runs casework, because the resolver refuses a file owned by
anyone else:
install -d -m 0700 /etc/registry-casework/secrets(umask 077; openssl rand -hex 32 | tr -d '\n' \ > /etc/registry-casework/secrets/casework-audit-key)(umask 077; printf '%s' 'postgresql://casework_runtime:<runtime-password>@db.example.org:5432/casework' \ > /etc/registry-casework/secrets/casework-runtime-database-url)(umask 077; printf '%s' 'postgresql://casework_migration:<migration-password>@db.example.org:5432/casework' \ > /etc/registry-casework/secrets/casework-migration-database-url)Each file must be a regular file owned by the effective user, with mode 0400 or 0600, exactly
one link and no symbolic link to it, at most 64 KiB, non-empty, and free of NUL bytes, so generate
key material as text rather than raw bytes. The name starts with a lowercase letter and carries
lowercase letters, digits, ., _, and -. The bytes are used exactly as written, neither
trimmed nor decoded, so write them without a trailing newline. The audit key needs at least 32
bytes, which 64 hexadecimal characters satisfy. Percent-encode a password that carries reserved
characters, and give the database user in each URL the role that owns the matching credential. A
reference that cannot resolve is refused by name, together with the rule it broke, and the
resolved bytes never appear in a message or a log.
The external audit file is a keyed hash chain, and the runtime authenticates the retained chain before it appends to it. A lost or replaced key leaves every retained record unverifiable and stops the process from continuing that file, and no command reconstructs it. Back the key up separately from the audit storage, and keep both for as long as the records must stay provable.
Verify the package before it opens the listener
Section titled “Verify the package before it opens the listener”The runtime verifies the package every time it loads the runtime file, before it opens the
database, contacts the issuer, or binds the listener. It reads casework.package.json beside
casework.yaml, recomputes the policy digest over the sorted file list, checks every file’s
SHA-256 digest and byte count, and refuses a package directory holding any file the manifest does
not declare or any symbolic link. A start that verifies the package records the policy digest in
the runtime log before anything else happens.
With tlsTermination: operator-controlled-upstream, an absent manifest is a refusal rather than a
fallback to the authored project. Each of these refusals is one run’s entire output on standard
error, written before the listener binds, and each exits non-zero:
casework: operator-controlled production requires a verified Casework policy packagecasework: the Casework policy package is invalidcasework: the Casework runtime configuration is not valid YAMLcasework: the Casework runtime configuration is invalidcasework: the Casework secret-provider configuration is invalid; secretProviders.file.root must be an absolute pathThe first line reports a production configuration with no manifest. The second reports a manifest
whose digests, byte counts, or file set do not match the directory. The third and fourth report the
runtime file itself: unparseable YAML, and a configuration the checks refuse, which covers an
invalid listener boundary, an empty issuer or claim name, a human-identity claim equal to the scope
claim, an access profile using the human-identity claim as its principal claim, and a sources map
that does not match the declared source ids exactly. The fifth reports a secret provider root the
resolver cannot use.
Migrate and serve
Section titled “Migrate and serve”Apply the migrations with the migration credential, from the operator host:
casework --runtime-config /etc/registry-casework/runtime.yaml migratemigrate loads the same runtime file, resolves migrationUrlRef, takes a PostgreSQL advisory
lock, and applies every unapplied migration in order under its ledger, so a second migrate on the
same database waits rather than racing it. Run it before the first serve and before rolling out a
release that adds migrations.
The migration credential owns the schema, and the serving process never needs it. Supply it only for the duration of a migration, and keep the runtime credential as the one the service manager holds.
Then run the service:
casework --runtime-config /etc/registry-casework/runtime.yaml serveRun serve under your service manager and let it restart the process. serve refuses to start on
an unverified production package, a listener the declared boundary does not allow, source bindings
that do not match the declared sources, a database reference or connection it cannot use, an OIDC
issuer it cannot initialize, and an audit secret or audit file it cannot open. Once it is serving,
the listener stops when any supervised background worker stops, and the process exits reporting the
stopped worker, because a deployment whose maintenance, reconciliation, or audit publication loop
is gone keeps neither its deadlines nor its accountability records.
Probe GET /health for liveness: it answers 200 as long as the process runs. Probe GET /ready
before routing traffic: it answers 503 while audit publication is unhealthy or PostgreSQL is
unreachable, and 200 otherwise. Readiness checks the audit publisher first, then the store.
The audit publication worker runs once a second: it reads pending records, appends them to the
keyed chain, and marks them published. A failed pass makes readiness fail, logs only the stage that
failed (pending-read, record-identity, sink-append, or published-mark) rather than the
record, and retries; a passing run restores readiness. A maintenance pass runs every two seconds
and a source reconciliation pass per bound source at that binding’s
reconciliationIntervalMilliseconds, every 60 seconds unless set. A source outage stays visible
on the affected work-item operations rather than turning an inbox into an apparently complete empty
page.
Establish directory authority
Section titled “Establish directory authority”A serving process has no directory until an Administrator creates one, and a queue with no serving
team holds work nobody can reach. Bootstrap the first team with an Administrator access token that
carries the human-identity assertion. If-Match carries the directory revision the caller expects,
which is "0" for the first call, and Idempotency-Key binds the request to one exact mutation:
POST /v1/directory/bootstrap HTTP/1.1Authorization: Bearer <administrator-access-token>Registry-Casework-Profile: administratorIf-Match: "0"Idempotency-Key: <caller-selected-key>Content-Type: application/json
{ "teamId": "licence-review", "staff": [ { "issuer": "https://identity.example.org/realms/registry", "subject": "<staff-subject>" } ], "supervisors": [ { "issuer": "https://identity.example.org/realms/registry", "subject": "<supervisor-subject>" } ], "queueId": "corrections"}Afterwards, replace one team at a time with PUT /v1/directory/teams/{team_id}, carrying the same
three headers with If-Match set to the revision the last directory response reported, and a body
of staff, supervisors, and servedQueues. The replacement is whole: the members and queues it
names become the team. A served queue belongs to one team, so a queue another team already serves
is refused with precondition.failed; remove it from that team first, then assign it. A team holds
at most 100 staff, 100 supervisors, and 100 served queues, and the whole directory document fits
2 MiB. Authority changes take effect immediately, newly ineligible held items are released through
maintenance, and items with an unresolved source attempt stay held for a later retry. Administrator
authority covers the directory and holiday maintenance only; it grants no item payload access and
no source review or application authority.
Check readiness with doctor
Section titled “Check readiness with doctor”caseworkctl doctor opens every live dependency the runtime opens, without binding a listener:
caseworkctl --format json doctor --runtime-config /etc/registry-casework/runtime.yamlIt checks the configuration, the exact imported source descriptions, each source connection and its reader grants, the database, the OIDC issuer, and directory readiness, and it stops at the first one that refuses and names it. Its secret preflight resolves the audit reference before it opens anything, and a reference that cannot resolve is reported by name with the rule it broke. Directory readiness requires a team serving every queue the package declares, and a gap is reported as an instruction to complete the queue assignments as an Administrator. A run that reaches the end prints a JSON report naming each check and each source it contacted.
Change the active package
Section titled “Change the active package”The runtime verifies and loads the package once, at startup. Install the reviewed successor into a
new directory beside the current one, point package.root at the new directory, and
restart or roll out the process. Keep the previous package directory until the new one serves, so a
rollback is a configuration change rather than a rebuild.
A running process keeps the policy it verified at startup, so files replaced under it leave the deployment serving a package that no longer matches its own manifest, and the next restart refuses the directory. Install the successor beside the current package and restart.
Activating a package does not rewrite running clock occurrences: each keeps the clock policy and
calculation pinned when it started. Holiday changes are a separate, deliberate operation. An
Administrator publishes an immutable holiday-set revision, then previews and applies the change in
batches of at most 100 active occurrences, repeating both until every occurrence is pinned to the
selected revision. A preview records the expected calculation generation for each occurrence and is
bound to the actor and selected profile for 15 minutes; an expired preview returns
clock.recompute-preview-expired, and an already-applied preview or a changed generation returns
precondition.failed. Create and review a new preview after either response.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Next move |
|---|---|
operator-controlled production requires a verified Casework policy package, or the Casework policy package is invalid | The first names a production configuration with no manifest, the second a package directory whose files disagree with the manifest. Point package.root at the installed package, and reinstall it from the reviewed artifact rather than editing it. |
the Casework runtime configuration is invalid | Check the listener against tlsTermination and networkExposure, the non-empty issuer and claim names, the human-identity claim against scopeClaim and the package’s profiles, and sources against the declared source ids. |
the Casework secret-provider configuration is invalid | The file provider root is relative or unusable. Write an absolute root. |
| A secret reference is refused | The named file must be a regular file owned by the running user, mode 0400 or 0600, one link, no symbolic link, non-empty, and free of NUL bytes. |
Startup refuses the database or doctor stops at it | Check the resolved URL’s user and database name, TLS on the server, and the trusted root reference when the server uses a private authority. |
| Startup refuses the OIDC issuer | Check discovery reachability from the Casework host, or the pinned JWKS document’s keys and their distinct kid values. |
GET /ready returns 503 while GET /health returns 200 | Audit publication or PostgreSQL is failing. Read the runtime log for the failed audit stage, and check the audit file, its lock, and the database. |
profile.not-human on a token you believe is human | The issuer did not add the configured human-identity claim to that session. Check the claim name and value against the issuer’s mapping for interactive sessions. |
| Work items report a source outage | The bound source is unreachable or refusing the reader. Run caseworkctl doctor, which names the failing source, then check that binding’s base URL, token endpoint, and reader grants. |
- Author a Casework policy for the package this deployment serves, and the checks that produce it.
- Erase and settle retained source work for the operator commands that erase source-backed payload copies and settle an uncertain source attempt.
- How Casework works for the model behind queues, claims, attempts, and the directory.
- Read the Casework API for the exact headers, request schemas, and recovery problems every operation returns.