Configuration Reference
On this page
All CRAIG services are configured via environment variables using the pattern CRAIG_<SERVICE>__<FIELD> (double underscore separator).
The config crate parses these with try_parsing(true), so numeric and boolean values are automatically converted from strings.
(Exception: craig-seed reads its variables via raw std::env::var with bespoke fail-closed handling — see its section.)
|
Audited 2026-08-08 (#1050): every row below was verified against the loading code
( |
Deployment-Global Variables (no service prefix)
Read by multiple services (or the whole fleet) under a bare CRAIG_/standard name — set these once per deployment.
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
CRAIG__ACTIVE_STATE_BUNDLES |
String |
single-bundle builds auto-resolve |
Yes (see description) |
ADR-032 §2.7 state-bundle activation: must name EXACTLY ONE compiled-in bundle ( |
CRAIG_PEER_JWKS_JSON / CRAIG_PEER_JWKS_JSON_FILE |
String (JSON array) |
empty registry |
Effectively yes for S2S actor flows |
Plan E peer-verifier registry: |
CRAIGREQUEST_CLAIMSWINDOW_DAYS |
u32 |
|
No |
ADR-062 §B: |
CRAIG_IDENTITY_SERVICE_ROLE_PREFIX |
String |
|
No |
Role prefix marking service-principal callers in claims ( |
CRAIG_FIELD_ENCRYPTION_KEY |
String |
— |
cases |
32-byte base64 field key shared by craig-cases + craig-seed — see the craig-cases extras row and the craig-seed section for the (deliberately different) failure semantics |
RUST_LOG |
String |
— |
No |
Standard tracing env filter. When set AND parseable it fully OVERRIDES the per-service |
OTEL_EXPORTER_OTLP_ENDPOINT |
String |
— |
No |
When set (and the default-on |
Shared Configuration (8 Core Services)
Services: craig-rules, craig-cases, craig-placement, craig-exchange, craig-financial, craig-reporting, craig-security, craig-composition.
Prefix: CRAIG_<SERVICE> (e.g., CRAIG_RULESPORT=8001)
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
PORT |
u16 |
— |
Yes |
Service listening port. The bind address is hardcoded |
DATABASE_URL |
String |
— |
Yes |
PostgreSQL connection URL. Also read directly by the same-image |
RABBITMQ_URL |
String |
— |
Yes |
RabbitMQ AMQP connection URL. #1202: each service connects as its OWN least-privilege broker account ( |
OIDC_ISSUER |
String |
— |
Yes |
Keycloak realm issuer URL (JWT |
OIDC_INTERNAL_URL |
String |
— |
No |
Internal Keycloak URL for JWKS discovery (falls back to issuer) |
CLIENT_ID |
String |
— |
Yes under |
OIDC |
CLIENT_SECRET |
String |
— |
paired with CLIENT_ID |
Secret for the service principal |
SIGNING_JWK / SIGNING_JWK_FILE |
String (inline ES256 private JWK / secret-mount path) |
— |
No (as a set) |
Plan E actor-identity keypair: with SIGNING_KID it wires the |
SIGNING_KID |
String |
— |
iff a JWK source is set |
|
TOKEN_VALIDATION_MODE |
|
|
No |
ADR-029 token-validation dispatch. |
INTROSPECTION_CACHE_TTL_SECONDS |
u64 |
|
No |
Introspection verdict cache TTL |
INTROSPECTION_CACHE_MAX_ENTRIES |
usize |
|
No |
Introspection cache size bound |
INTROSPECTION_SERVE_ON_OUTAGE |
bool |
|
No |
Serve cached verdicts during an introspection outage. Default fail-closed |
INTROSPECTION_ENDPOINT |
String |
discovery |
No |
Explicit introspection endpoint override (discovery wins when unset) |
INTROSPECTION_AUTH_METHOD |
|
|
No |
How the service authenticates to the introspection endpoint |
LOG_LEVEL |
String |
|
No |
Logging level (debug, info, warn, error). A set-and-parseable |
JURISDICTION |
String |
— |
Yes |
Jurisdiction identifier for rule set selection (Plan A §12 removed the silent |
CORS_ORIGINS |
String |
empty (same-origin only) |
No |
Comma-separated allowed CORS origins, or |
BODY_LIMIT |
usize |
|
No |
Maximum request body size in bytes |
RATE_LIMIT_RPM |
u32 |
|
No |
Per-IP request cap per MINUTE; |
TRUSTED_PROXIES |
String |
empty (no proxies trusted) |
No |
#766: comma-separated CIDRs of trusted upstream proxies (LBs, ingress, the BFF) for the rate limiter’s client-IP extraction — the craig-intake Plan C §D5 design, now fleet-wide via |
DB_MAX_CONNECTIONS |
u32 |
|
No |
Database connection pool size. Size against the server’s |
DB_IDLE_TIMEOUT_SECS |
u64 |
|
No |
Database connection idle timeout in seconds |
DB_STATEMENT_TIMEOUT_MS |
u64 |
|
No |
F-019: Postgres |
ADMIN_UNIT_LABEL |
String |
— |
Yes |
Display label for the admin-unit concept (County, Region, Chapter, etc.; Plan A §12 removed the silent |
AUTHZ_REQUIRE_FULL_COVERAGE |
bool |
|
No |
Boot-bail when the jurisdiction×resource-type authz matrix has uncovered tuples |
AUTHZ_POLICY_TTL_SECONDS |
u64 |
|
No |
Authz ruleset-cache TTL fallback for missed invalidation events |
AUTHZ_EVAL_TIMEOUT_MS |
u64 |
|
No |
Per-authz-decision eval budget; elapsing returns a typed 503 (#1126). Valid |
AUTHZ_FUNCTION_TIMEOUT_MS |
u64 |
|
No |
JS function-node interrupt bound published into zen’s PROCESS-GLOBAL config at boot (#1328); explicit values must sit in |
EVENT_OUTBOX_RETENTION_DAYS |
u32 |
|
No |
#965/#1129 (ADR-058): |
EVENT_INBOX_RETENTION_DAYS |
u32 |
|
No |
#965/#1129 (ADR-058): |
RETENTION_ARCHIVE__ENABLED |
bool |
|
No |
#1129 (ADR-058 D11): consent to archive-then-prune audit-class rows. Consumed by craig-security + craig-rules; gates the scheduled worker AND security’s |
RETENTION_ARCHIVE__HOT_WINDOW_DAYS |
u32 |
|
No |
#1129: archive-eligibility age (1..=3650, load-validated even while disabled). craig-exchange additionally floors it at ≥365 (#1466 B5 — the SSA screening quiescence horizon; the devstack compose sets it). Operational knob, NOT the DFCS records-retention schedule (⁂ #1073) |
RETENTION_ARCHIVE__INTERVAL_SECONDS |
u64 |
|
No |
#1129: scheduled archive-pass cadence; |
RETENTION_ARCHIVE__BATCH_SIZE |
u32 |
|
No |
#1129: rows per archive batch (1..=10000); one batch = one object + one crash-atomic prune transaction |
RETENTION_ARCHIVE__MAX_BATCH_BYTES |
usize |
|
No |
#1129: byte cap per archive object (>= 4096; boot-validated ⇐ the store’s MAX_UPLOAD_BYTES); a single row exceeding it alone quarantines loudly (D13) |
Service-Specific Extras
Some of these are technically parsed by every service’s shared settings struct but consulted by only one — they are listed under their consumer.
| Service | Variable | Default | Description |
|---|---|---|---|
all except craig-rules |
RULES_ENGINE_URL |
craig-rules base URL, read per-service ( |
|
craig-rules |
EVAL_TIMEOUT_MS |
|
Budget for one rules evaluation dispatch (queue wait + evaluation), milliseconds; elapsing returns 503 on |
craig-rules |
FUNCTION_TIMEOUT_MS |
|
Per-NODE interrupt bound for JDM JS function nodes, milliseconds (#1046 / ADR-006 §Amendment #1046) — published into zen-engine’s process-global config at boot, replacing the silently-inherited upstream 5s default. An explicit value must sit in |
craig-rules |
DECISION_REFRESH_SECONDS |
|
Decision-cache sweep interval, seconds (#1188 / the ADR-006 #1188 amendment). Valid |
craig-cases |
ENCRYPTION_MODE |
|
Field-encryption posture (ADR-020 / ADR-048): |
craig-cases |
|
— |
32-byte base64 field key, shared with craig-seed so seeded rows match service-written ones. In |
craig-cases |
JWS_REPLAY_TTL_HOURS |
|
TTL (hours) for |
craig-cases |
JWS_REPLAY_CLEANUP_INTERVAL_SECONDS |
|
Poll interval for the |
craig-exchange |
ICPC_DEADLINE_DAYS |
|
ICPC home-study deadline in calendar days (only consumed by the ICPC request update path) |
craig-financial |
CASES_URL |
craig-cases URL — placement-activation billing resolves the child’s DOB from cases at event time (#979) |
|
craig-financial |
PLACEMENT_BASE_URL |
#1069 M2: craig-placement base URL for the S2S subsidy-eligibility read at ERR creation. Fail-closed — placement unreachable ⇒ 503, never an unverified enrollment. Default suits local non-container runs; the devstack compose sets the service host |
|
craig-financial |
PAYMENT_PERIOD |
|
Payment-period boundary policy for per-diem billing rollups: |
craig-financial |
SUBSIDY_GENERATOR_INTERVAL_SECONDS |
|
#1068/ADR-053: monthly subsidy generation scheduler interval. 0 = disabled — enabling automated money generation is explicit deployment consent. Also requires |
craig-financial |
SUBSIDY_REVIEW_SWEEP__INTERVAL_SECONDS |
|
#1096/ADR-054 U4: review-sweep scheduler tick. 0 = no scheduler — sweep enforcement MUTATES agreement status, so the tick is explicit deployment consent (deliberate asymmetry with the always-on devstack generator). Also requires |
craig-financial |
SUBSIDY_REVIEW_SWEEP__MATERIALIZE |
|
#1096: scheduled sweeps materialize missing first review slots from term anchors |
craig-financial |
SUBSIDY_REVIEW_SWEEP__AUTO_SUSPEND |
|
#1096: enforce leg 1 — suspend agreements whose open review is past due + 30-day grace; recorded |
craig-financial |
SUBSIDY_REVIEW_SWEEP__AUTO_TERMINATE |
|
#1096: enforce leg 2 — terminate agreements suspended three months or more ( |
craig-financial |
SUBSIDY_REVIEW_SWEEP__AUTO_PER_DIEM_HANDOFF |
|
#1069 M3/ADR-055: enforce leg 3 — terminate an ERR agreement whose caregiver’s foster per diem began on the agreement’s own placement (reason |
craig-financial |
SUBSIDY_ERR__ENABLED |
|
#1069 M2 (F4): the one-shot ERR creation gate, nested as |
craig-financial |
SUBSIDY_SG__ENABLED |
|
#1070 (U4): the SG/NRSG guardianship ENROLLMENT-consent gate, nested as |
craig-financial |
SUBSIDY_IMPORT__ENABLED |
|
#1071/ADR-057 (D12): the closed-cohort conversion-import gate, nested as |
craig-reporting |
CASES_URL |
craig-cases URL — the NCANDS generate path fetches export rows from cases at request time (#652 Phase D) |
|
craig-security |
DETECTION_INTERVAL_SECS |
|
Periodic detection-scan interval, seconds (note the |
craig-composition |
RULESETS_DIR |
|
Root directory for jurisdiction baseline ruleset files consumed by the CompositionLoader; overridable for tests/non-standard deploys; absent baseline files are silently OK |
craig-intake (port 8008)
Prefix: CRAIG_INTAKE__
The intake service is a stateless edge in both modes (ADR-017): it owns no database and consumes no message queue — partner identity lives in craig-security, reports live in craig-cases, and every submission is forwarded over HTTP. There are therefore no DATABASE_URL/RABBITMQ_URL/DB_* variables for this service (#1211). Intake also requires the deployment-global CRAIG__ACTIVE_STATE_BUNDLES (its embedded public UI is bundle-themed — boot fails without it). The MODE variable selects how partner credentials are validated:
-
Integrated (default): full CRAIG deployment — partner keys validated via craig-security, submissions forwarded to craig-cases
-
Standalone: air-gapped edge — partner keys read from a local JSON file; forwards to a remote cases service, or with
BACKEND_PROFILE=shinesto a SHINES instance + keyring sidecar (ADR-042)
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
PORT |
u16 |
— |
Yes |
Service listening port |
MODE |
IntakeMode |
|
No |
Operating mode: |
BACKEND_PROFILE |
BackendProfile |
|
No |
ADR-042 durable-backend selection: |
OIDC_ISSUER |
String |
— |
Yes |
OIDC issuer URL (both modes — intake mints |
OIDC_INTERNAL_URL |
String |
— |
No |
Internal OIDC URL for split-DNS deployments (falls back to issuer) |
CLIENT_ID |
String |
— |
Yes |
OIDC |
CLIENT_SECRET |
String |
— |
Yes |
Secret for the service principal |
LOG_LEVEL |
String |
|
No |
Logging level |
JURISDICTION |
String |
— |
Yes |
Jurisdiction identifier — must be set explicitly (the silent Georgia default was removed) |
CORS_ORIGINS |
String |
|
No |
Allowed CORS origins ( |
BODY_LIMIT |
usize |
|
No |
Max request size (accommodates 10 MiB file upload + overhead) |
CASES_URL |
String |
— |
Unless SHINES |
craig-cases base URL — required in every cases-backed configuration (both modes); only the standalone-SHINES backend omits it |
SHINES_URL |
String |
— |
SHINES |
SHINES base URL (standalone |
KEYRING_URL |
String |
— |
SHINES |
|
SECURITY_URL |
String |
— |
Integrated |
craig-security base URL (partner verification + signer-key lookup); ignored in standalone |
IP_HASH_SECRET |
String |
— |
Yes |
Per-deployment HMAC secret for |
TRUSTED_PROXIES |
String (CSV CIDRs) |
empty |
No |
CIDRs whose X-Forwarded-For/Forwarded headers are trusted for client-IP resolution (rate limiting + audit hashing) |
CAPTCHA_SECRET |
String |
|
No |
CAPTCHA verification secret (set to |
CAPTCHA_VERIFY_URL |
String |
Cloudflare Turnstile URL |
No |
CAPTCHA verification endpoint (any siteverify-shaped endpoint works; the default is Turnstile’s) |
CAPTCHA_SITE_KEY |
String |
— |
No |
CAPTCHA site key for the embedded public UI (both modes — intake is THE public portal since epic &60 Phase 5) |
REQUIRE_CAPTCHA |
bool |
|
No |
Production guard — when |
PUBLIC_RATE_LIMIT |
u32 |
|
No |
Maximum public submissions per IP per hour |
SIGNED_RATE_LIMIT |
u32 |
|
No |
Requests per IP per hour on the standalone-SHINES signed nest |
SIGNED_MAX_FILE_BYTES |
usize |
|
No |
Per-file cap for signed-path multipart attachments (ADR-042 §D9) |
SIGNED_BODY_LIMIT |
usize |
|
No |
Whole-request body cap for the signed nest (overrides |
PARTNER_KEYS_FILE |
String |
— |
No |
Path to JSON file with partner API keys (standalone; absent ⇒ the partner API rejects all requests, warned at boot) |
TLS_CERT_FILE / TLS_KEY_FILE |
String |
— |
No |
ADR-046 optional in-process TLS — PEM cert/key paths, both-or-neither; default is plain HTTP behind an ingress terminator |
SELF_SIGNED_TLS |
bool |
|
No |
ADR-046 dev/test-only ephemeral self-signed cert; requires the |
DEBUG_EMIT_CPS_REQUEST |
String |
— |
No |
#713 dev/integration only — debug CpsRequest panel gate. Active ONLY when set to the exact value |
craig-intake-keyring (port 8010)
Prefix: CRAIG_INTAKE_KEYRING__
The keyring is the standalone-SHINES-only sidecar (ADR-042 §D8): a flat-file JSON signer-key registry + JWS replay dedup. No database, no message queue, no OIDC — it consumes none of the shared variables. Admin lifecycle routes fail closed when ADMIN_TOKEN is unset.
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
PORT |
u16 |
— |
Yes |
TCP listen port (ADR-042 §D8 convention: 8010); |
LOG_LEVEL |
String |
|
No |
Tracing level |
STORE_PATH |
String |
— |
Yes |
Path to the flat-file JSON store. Parent directory created on load, file created if absent; writes are crash-safe (tmp-write → fsync → rename) |
ADMIN_TOKEN |
String |
— |
No |
Bearer token gating the approve/revoke lifecycle routes. Absent ⇒ those routes fail-closed reject everything (boot warn names this var); set-but-blank is a load error so an empty value can never authorize. Debug-redacted |
REPLAY_RETENTION_SECS |
u64 |
|
No |
JWS replay-record retention: records with |
MAX_KEYS |
usize |
|
No |
Cap on stored signer keys; |
ENABLE_KEY_LIST |
bool |
|
No |
#714: mounts the metadata-only |
craig-web (port 8080)
Prefix: CRAIG_WEB__
The web service is a BFF (Backend-For-Frontend) that proxies requests to backend services and renders HTML via Askama templates. It also requires the deployment-global CRAIG__ACTIVE_STATE_BUNDLES.
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
PORT |
u16 |
— |
Yes |
Web UI listening port |
OIDC_ISSUER |
String |
— |
Yes |
Keycloak issuer URL for JWT validation |
OIDC_INTERNAL_URL |
String |
— |
No |
Internal Keycloak URL for OIDC discovery |
KEYCLOAK_CLIENT_ID |
String |
— |
Yes |
Keycloak client ID for the user-facing code-flow login (e.g., |
KEYCLOAK_REDIRECT_URI |
String |
— |
Yes |
OAuth redirect URI |
CLIENT_ID |
String |
— |
No |
craig-web’s OWN |
CLIENT_SECRET |
String |
— |
paired |
Secret for CLIENT_ID; both must be non-empty to wire the service-token source |
SIGNING_JWK / SIGNING_JWK_FILE / SIGNING_KID |
String |
— |
No (as a set) |
The BFF’s actor-issuer keypair ( |
RULES_URL |
String |
— |
Yes |
craig-rules service URL |
CASES_URL |
String |
— |
Yes |
craig-cases service URL |
PLACEMENT_URL |
String |
— |
Yes |
craig-placement service URL |
EXCHANGE_URL |
String |
— |
Yes |
craig-exchange service URL |
FINANCIAL_URL |
String |
— |
Yes |
craig-financial service URL |
REPORTING_URL |
String |
— |
Yes |
craig-reporting service URL |
SECURITY_URL |
String |
— |
Yes |
craig-security service URL |
COMPOSITION_URL |
String |
— |
Yes |
craig-composition backend base URL (Plan X / ADR-035 composed surfaces) |
SESSION_SECRET |
String |
— |
Yes |
AES-GCM master key for the stateless session cookie ( |
SESSION_MAX_AGE_SECS |
i64 |
|
No |
Session cookie lifetime in seconds (default: 30 minutes). Increase for longer idle timeouts; decrease for stricter security. |
SESSION_SECURE |
bool |
|
No |
Emit the |
LOG_LEVEL |
String |
|
No |
Logging level |
DEFAULT_LOCALE |
String |
|
No |
Default i18n locale. Only |
THEME |
String |
— |
Yes |
UI theme name (CSS theme identifier; Plan A §12 removed the silent |
BRANDING_AGENCY |
String |
— |
Yes |
Agency name for branding (Plan A §12 removed the silent default) |
BRANDING_APP_NAME |
String |
|
No |
Application name for branding |
BRANDING_LOGO_URL |
String |
— |
No |
Custom logo URL (defaults to theme logo) |
PLUGIN_ALLOWED_HOSTS |
CSV list |
empty (deny ALL plugin fetches) |
No |
Fail-closed SSRF allow-list for plugin outbound fetches (ADR-033 §6). Never list loopback/metadata hosts |
UPLOAD_BODY_LIMIT |
usize |
|
No |
Max multipart request-body size (bytes) for the two BFF upload routes (case-contact attachments, placement home documents). axum’s built-in 2 MiB default otherwise rejects routine multi-MB scans / photos at the edge (#970). Applied per-route — every other BFF route keeps the 2 MiB default. Must be ≤ the storing backend’s |
|
A file upload traverses up to four request-body caps — ingress/reverse-proxy → BFF ( |
Object Store (CRAIG_STORE__)
Prefix: CRAIG_STORE__
Consumed by craig-cases, craig-exchange, craig-placement (home documents), craig-reporting (AFCARS/NCANDS exports), and craig-security — plus craig-rules when RETENTION_ARCHIVE__ENABLED=true, and the cargo xtask archive-fetch tooling. The retention D10 boot probe refuses the local backend when archiving is enabled.
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
BACKEND |
enum |
|
No |
Backend type: |
BUCKET |
String |
— |
S3 |
S3 bucket name (enforced when the S3 client is built, not at config load) |
LOCAL_ROOT |
String |
|
No |
Root directory for local filesystem backend (created on startup) |
S3_ENDPOINT |
String |
— |
S3 |
S3 endpoint URL (e.g., |
S3_REGION |
String |
|
No |
S3 region |
S3_ACCESS_KEY |
String |
— |
S3 |
S3 access key (Debug-redacted) |
S3_SECRET_KEY |
String |
— |
S3 |
S3 secret key (Debug-redacted) |
MAX_UPLOAD_BYTES |
usize |
|
No |
Maximum upload file size. Boot-validated ≥ |
|
Path-style addressing and |
craig-seed (fixture generator, tooling)
The craig-seed generator (invoked by cargo xtask dev) is a pure SQL-file generator: no database
connection, no HTTP. It reads its variables via raw std::env::var, fail-closed on malformed values,
rather than through the config crate. Since C7 activation
(#987) the devstack mounts the shared
devstack/field-key.env into it and runs it required, so every devstack seed writes genuine
ciphertext (verified by cargo xtask verify-seed --expect keyed).
Where the key comes from differs by environment. Locally it is ADOPTED from the repo-committed
encrypted store (secrets/dev.yaml, ADR-064) when the developer’s age identity is a store recipient,
with a generate-once fallback for identity-less contributors. In CI,
devstack/ci/write-field-key.sh materializes it by decrypting that same store with the
masked+protected CRAIG_CI_AGE_KEY CI/CD project variable inside the pinned devtools container
(ADR-048 §D5 as amended by ADR-064 U7,
#1385). An unset variable is a hard
error, and CRAIG_CI_AGE_KEY is a CI pipeline variable, never a service env var.
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
CRAIG__ACTIVE_STATE_BUNDLES |
String |
— |
Yes |
Selects the seed’s jurisdiction + rate schedule from the compiled-in bundle (supersedes the old |
CRAIG_SEED__ENCRYPTION_MODE |
String |
|
No |
Accepts only |
CRAIG_FIELD_ENCRYPTION_KEY |
String |
— |
In |
32-byte base64 field key, shared with craig-cases so seeded rows match service-written ones. Absent ⇒ plaintext seed (optional mode only); a malformed/wrong-length key is fatal in both modes (never silently seeds plaintext under a broken key). |