Skip to content
Registry StackDocsv0.34.0

Deploy Registry Casework

View as Markdown

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.

  • 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: human by 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, and secret:env/<NAME> names an environment variable.
  • Durable storage for the audit file. The runtime appends a keyed hash chain to one JSONL file, seals it under an eight-digit numeric suffix once it reaches 10 MiB, and deletes no sealed file. It holds a process-lifetime lock on a sentinel file beside it, so exactly one process writes one chain. The directory must belong to the runtime user with mode 0700.
  • A host for one process, or a place to run the container image.

A submitted-context review deployment that declares no source needs no Base Registry Engine (BReg): the review requests 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 binaries on the host that will serve, and on the operator host that holds the migration credential:

Terminal window
curl -fsSL https://github.com/registrystack/registry-stack/releases/latest/download/casework-install.sh | bash
casework --version
caseworkctl --version

The installer verifies every downloaded binary against the release SHA256SUMS before anything reaches the install directory, and installs casework and caseworkctl together or not at all. 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. migrate is a casework subcommand, so the same image runs it, given the migration credential instead of the runtime one; doctor belongs to caseworkctl, so it still runs from a separate operator host that has that binary installed. 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.

Create two login roles: a migration role that owns the schema and applies migrations, and a runtime role the serving process and background workers use. Keep the migration credential off the serving host when no migration is in progress.

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;

Casework’s migrations create ordinary tables in the database’s default public schema and declare no extension and no other schema, so the migration role owns that one schema and the runtime role only reads and writes through it. As an administrator, in the casework database:

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;
GRANT USAGE ON SCHEMA public TO casework_runtime;
ALTER DEFAULT PRIVILEGES FOR ROLE casework_migration IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO casework_runtime;
ALTER DEFAULT PRIVILEGES FOR ROLE casework_migration IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO casework_runtime;

Those default privileges cover every table and sequence migrate creates afterwards, because it always runs as the migration role. After the first migrate, or when reusing a database an earlier session already migrated, grant the runtime role the same privileges on the objects that already exist:

GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO casework_runtime;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO casework_runtime;

Casework creates no extension, no schema, and no role of its own, so a missing grant here surfaces as a permission-denied database error at serve rather than at migrate.

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/v1alpha1
kind: CaseworkRuntimeConfig
package:
# The selected package always contains the policy at casework.yaml.
root: /etc/registry-casework/package
# Optional: the policyDigest of the package you reviewed; any other package is refused.
expectedPolicyDigest: sha256:0000000000000000000000000000000000000000000000000000000000000000
listener:
# 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-address
secretProviders:
file:
# Owner-only files, one per reference.
root: /etc/registry-casework/secrets
database:
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.pem
authentication:
oidc:
issuer: https://identity.example.org/realms/registry
audience: urn:example:casework
scopeClaim: scope
humanIdentity:
claim: registry_actor_kind
value: human
jwksSource:
kind: discovery
audit:
path: /var/lib/registry-casework/audit/casework.ndjson
hashKeyRef: secret:file/casework-audit-key
sources:
# One entry per source the package declares, keyed by its exact source id.
professional-licences:
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
SectionWhat it binds
packageThe absolute root of the package containing casework.yaml, its manifest, and exact source descriptions, and optionally the one policy digest the runtime may load from it.
listenerThe listener address and the transport boundary the deployment declares for it.
secretProvidersThe explicitly enabled file and environment secret providers.
databaseSecret references for the runtime and migration connection URLs, and an optional trusted root certificate for the PostgreSQL connection.
authenticationThe OpenID Connect issuer, audience, claim names, human-identity assertion, and the source of the issuer’s keys.
auditThe path of the external JSONL chain and the reference to the key that chains it.
sourcesOne 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. Stock ThunderID emits the standard OAuth scope claim, so set scopeClaim: scope explicitly when using that issuer. 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 admits one access-token type, the RFC 9068 media type spelled at+jwt or application/at+jwt. A token carrying any other typ, a plain JWT included, is refused, so an issuer that mints ordinary JWTs for this audience must be configured to mint the access-token type.

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-jwks

The 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 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. If one pass outlasts the interval, Casework skips missed ticks instead of replaying them back-to-back against the source.

A binding has no event type to set. Each lifecycle event carries, as its ce-type, the id of the hook caseworkctl source add wrote on the request entity, casework-lifecycle-v1-<entity>, and Casework refuses an event whose type is not the one derived from the request entity its body names. A binding that still carries eventType is refused at startup.

Every source-backed work item, task grant, and saved attempt carries the source’s binding generation, computed from the source id, the binding’s eventSource, and the digest of the imported source description. Changing one of those three supersedes the source’s open work items and opens fresh ones on the next observation, because the source now means something else. Every other binding field is operational: rotating a client key, moving tokenEndpoint or baseUrl, changing resource, scopes, readerProfile, the trusted root, a timeout, the interval, displayReference, or contextProjection keeps the generation, and with it every in-flight work item, claim, and durable attempt.

Create each file as the user that runs casework, because the resolver refuses a file owned by anyone else:

Terminal window
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.

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.

A verified package proves its files match its own manifest, not that it is the package you reviewed. Set package.expectedPolicyDigest to the policyDigest that caseworkctl package reported for the reviewed package, and the runtime starts only on that package: a package naming another digest, or a directory with no manifest, is refused with both digests named, so a replaced package directory cannot change the policy a restart loads.

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 package
casework: the Casework policy package is invalid
casework: package.expectedPolicyDigest is sha256:4f0c…, but package.root holds the package with policy digest sha256:9b2e…
casework: the Casework runtime configuration is not valid YAML
casework: the Casework runtime configuration is invalid
casework: the Casework secret-provider configuration is invalid; secretProviders.file.root must be an absolute path

The 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 reports a verified package other than the one package.expectedPolicyDigest names, shown here with both digests shortened. The fourth and fifth 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 sixth reports a secret provider root the resolver cannot use.

Apply the migrations with the migration credential, from the operator host:

Terminal window
casework --runtime-config /etc/registry-casework/runtime.yaml migrate

migrate 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 unified review schema arrives in a single migration that replaces the experimental hosted-item tables of Casework 0.32.0 and earlier. No data is carried over, and this release has no in-place legacy conversion command. When those tables are empty, migrate replaces them. When any of them still holds a row, such as an in-flight hosted item or a retained accountability record, migrate refuses before applying anything and writes nothing. The refusal begins the Casework database holds hosted work that schema migration 15 would drop, then names each table that holds rows with its row count, such as casework_hosted_items (1 row).

Keep that database with the release that wrote it long enough to export the work it holds, take the normal backup, and provision a fresh Casework database for the unified review package. Do not delete rows, edit the migration ledger, or run migration SQL by hand to get past the refusal. For a disposable local session, use caseworkctl dev stop PROJECT --remove and start it again.

Upgrading from Casework 0.33.0 or earlier supersedes the open work items of BReg sources once. That release also computed the source binding generation from the credential, transport, and presentation fields, so the generation stored with each work item differs from the one this release computes, and each source’s next observation opens fresh work items beside the superseded ones. Claims, drafts, and pending attempts on the superseded items do not carry over. Finish or settle source-backed work before upgrading.

Upgrade from a release that rotated the audit file

Section titled “Upgrade from a release that rotated the audit file”

Casework 0.33.0 and earlier rotated the audit file: a full file became casework.ndjson.1, older files moved up to casework.ndjson.49, and the oldest was deleted. This release seals a full file as casework.ndjson.00000001 and keeps every sealed file, and it continues only a chain whose first retained record is the chain’s first. Before the first serve of this release:

  1. Stop every earlier casework process. While one still holds the lock beside the audit file, the new process refuses to start with another process holds the single-writer lock.

  2. List the audit directory. If no numbered file such as casework.ndjson.1 sits beside the audit file, the file never reached 10 MiB, and the new release continues its chain as it is. If one does, move the audit file and every numbered file beside it into an archive directory outside the audit directory:

    Terminal window
    install -d -m 0700 /var/lib/registry-casework/audit-archive
    cd /var/lib/registry-casework/audit
    mv casework.ndjson casework.ndjson.[0-9]* /var/lib/registry-casework/audit-archive/

    The new process then starts a fresh chain at the configured path. The archive stays keyed with the same audit key, so keep both for as long as your retention policy requires. An event the earlier process appended but never marked published appears again at the start of the fresh chain. Without the move, the new process refuses to start and names the numbered file it found.

  3. Make the audit directory mode 0700, and the audit file and its .lock file mode 0600, all owned by the runtime user. The earlier release accepted a group-readable directory; this one refuses it with audit directory must be owner-only.

Then run the service:

Terminal window
casework --runtime-config /etc/registry-casework/runtime.yaml serve

Set CASEWORK_LOG to choose the operational log level: error, warn, or info, which is the default when the variable is unset. Any other value, including a tracing filter directive that would enable a dependency’s own debug or trace logging, refuses to start. serve writes structured JSON to standard output at every level, so redirecting it to a file never carries colour codes, and the level applies to Casework’s own logging only; the ambient RUST_LOG variable has no effect on this process.

A restart of this same version against the same package, runtime file, and database is the normal recovery path after process or host interruption. It reloads the verified package and resumes the durable review, clock, result-delivery, and reconciliation state. Run migrate again only when the release procedure calls for it; never use a restart as a legacy-state conversion.

Run 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. When the source reader fails, the runtime logs Casework source reader request to BReg failed with the cause, such as the refusal status and problem code or the token request failure, once when the cause appears or changes. A missing record is not a reader failure and is not logged; a 404 from the registry contract, readiness, or a list is. The next successful reader request logs Casework source reader requests to BReg succeed again. That entry means BReg answers the reader again, not that reconciliation has caught up: a pass that still cannot apply the source logs Casework reconciliation pass did not complete.

Between a source change and the reconciliation that applies it, an item whose binding moved within the same source generation stays in the inbox, the next-item result, and holdings, and its view, history, and clocks stay readable with the binding Casework last applied. It offers no actions. Operations that act on the item or its tasks are refused with work-item.proposal-changed: claim, release, drafts, decisions, attempt recovery, assignment, delegation, task preview, listing, and approval, and caseload move apply. A caseload move preview is refused as a whole while such an item is among the items it reads.

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.1
Authorization: Bearer <administrator-access-token>
Registry-Casework-Profile: administrator
If-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.

caseworkctl doctor opens every live dependency the runtime opens, without binding a listener:

Terminal window
caseworkctl doctor --runtime-config /etc/registry-casework/runtime.yaml

It 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 report naming each check and each source it contacted.

doctor answers whether the deployment is ready to do work. It does not summarize an individual review. Inspect the Casework request, task, context, result, and result-feed resources for review progress, and the BReg request’s data.request.review projection for submission, delivery, application, and recovery state. A durable submission reports its fixed recoveryDeadline. Automatic application jobs report bounded attempts and expose nextAttemptAt only while queued or applying. Use application.state with recovery.code for recovery decisions. Do not query either product’s database directly.

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 package or source binding change that alters the source binding generation supersedes the source’s open work items. Rolling that change back does not reopen the work items it superseded: they stay superseded, and the source’s next observation opens fresh work items beside them. A change to operational binding fields alone keeps the generation and the work items.

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.

SymptomNext move
operator-controlled production requires a verified Casework policy package, or the Casework policy package is invalidThe 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 invalidCheck 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 invalidThe file provider root is relative or unusable. Write an absolute root.
A secret reference is refusedThe 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 itCheck 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 issuerCheck discovery reachability from the Casework host, or the pinned JWKS document’s keys and their distinct kid values.
the Casework database schema version N is newer than this binary supports (M)A newer release already migrated this database. Run that release or a later one; Casework does not migrate a schema down.
the Casework database holds hosted work that schema migration 15 would dropThe database still holds hosted work from Casework 0.32.0 or earlier. Keep it with that release until its work is exported, then migrate a fresh database as Migrate and serve describes.
the Casework audit journal could not be initialized naming a numbered file rotated by an earlier Casework release, or saying the file does not begin at the first record of its chainThe audit directory still holds the layout of Casework 0.33.0 or earlier. Archive it as the upgrade steps describe.
the Casework audit journal could not be initialized: audit directory must be owner-onlySet the audit directory to mode 0700, owned by the runtime user.
GET /ready returns 503 while GET /health returns 200Audit 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 humanThe 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 outageThe bound source is unreachable or refusing the reader. Run caseworkctl doctor, which names the failing source, read the runtime log’s Casework source reader request to BReg failed entry for the cause, then check that binding’s base URL, token endpoint, and reader grants.