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 (ServiceSettings, per-service config structs, bootstrap, raw-env sites). Future drift is measured from this date.

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 (georgia, tx-stub); unset/empty/multiple/unknown all FAIL BOOT for every consumer. Consumers: craig-financial (#1072 — the feature matrix), craig-exchange (bundle orchestrator), craig-reporting (federal mapping), craig-intake + craig-web (embedded UI/bundle surfaces), craig-composition, and craig-seed. The devstack compose supplies georgia

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: [{iss, kid, jwk}] public keys for verifying inbound X-Craig-Actor JWTs. Empty ⇒ every inbound actor header fail-closes (401). File form for secret mounts; inline wins when both set

CRAIGREQUEST_CLAIMSWINDOW_DAYS

u32

30

No

ADR-062 §B: request_claims prune horizon, deliberately fleet-wide (no per-service leg). 0 disables the pruner (registered Disabled in /healthz); an UNPARSEABLE value also disables it with an error log — never guesses

CRAIG_IDENTITY_SERVICE_ROLE_PREFIX

String

service:

No

Role prefix marking service-principal callers in claims (service:craig-web etc.; ADR-028). Read per call, works across any OIDC backend emitting a roles-style claim

CRAIG_FIELD_ENCRYPTION_KEY

String

 — 

cases required mode

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 LOG_LEVEL; also honored by the <binary> migrate gate’s logging-only telemetry

OTEL_EXPORTER_OTLP_ENDPOINT

String

 — 

No

When set (and the default-on otel feature is compiled) enables OTLP trace export + Prometheus metrics with the service name as the OTel resource; unset ⇒ JSON logging only

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 0.0.0.0 — there is no listen-address knob

DATABASE_URL

String

 — 

Yes

PostgreSQL connection URL. Also read directly by the same-image <binary> migrate gate (ADR-063)

RABBITMQ_URL

String

 — 

Yes

RabbitMQ AMQP connection URL. #1202: each service connects as its OWN least-privilege broker account (craig-<svc>), never a shared identity — provision per the deployment guide’s User Configuration section

OIDC_ISSUER

String

 — 

Yes

Keycloak realm issuer URL (JWT iss claim)

OIDC_INTERNAL_URL

String

 — 

No

Internal Keycloak URL for JWKS discovery (falls back to issuer)

CLIENT_ID

String

 — 

Yes under auto/introspect token validation

OIDC client_credentials service principal for ALL outbound S2S calls (Plan E §4 — the cases auto-link consumer, financial→placement reads, introspection bearer). Under the DEFAULT TOKEN_VALIDATION_MODE=auto, boot hard-fails without the pair (the error names these vars); only a pinned jws mode boots without (warn)

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 ActorTokenIssuer so outbound S2S calls carry X-Craig-Actor. All-absent ⇒ no issuer (boot warn, actor headers can’t be minted); inline JWK wins when both forms set; empty strings normalize to absent

SIGNING_KID

String

 — 

iff a JWK source is set

kid advertised for the signing key. One half of the pair without the other is a BOOT ERROR

TOKEN_VALIDATION_MODE

jws | introspect | auto

auto

No

ADR-029 token-validation dispatch. auto/introspect require the CLIENT_ID/CLIENT_SECRET pair (introspection authenticates as the service principal)

INTROSPECTION_CACHE_TTL_SECONDS

u64

60

No

Introspection verdict cache TTL

INTROSPECTION_CACHE_MAX_ENTRIES

usize

10000

No

Introspection cache size bound

INTROSPECTION_SERVE_ON_OUTAGE

bool

false

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

basic | bearer

basic

No

How the service authenticates to the introspection endpoint

LOG_LEVEL

String

info

No

Logging level (debug, info, warn, error). A set-and-parseable RUST_LOG overrides it entirely

JURISDICTION

String

 — 

Yes

Jurisdiction identifier for rule set selection (Plan A §12 removed the silent georgia default). Also selects the business calendar (#1092/ADR-053 amendment): financial date bounds and month anchors resolve through craig_common::BusinessClockgeorgiaAmerica/New_York; unmapped values run on UTC. The timezone is canonical per-jurisdiction data (one mapping arm in craig-common), never a separate knob

CORS_ORIGINS

String

empty (same-origin only)

No

Comma-separated allowed CORS origins, or for any. Each non- entry is boot-validated as scheme://host — a malformed entry FAILS STARTUP (#816)

BODY_LIMIT

usize

2097152 (2 MiB)

No

Maximum request body size in bytes

RATE_LIMIT_RPM

u32

600

No

Per-IP request cap per MINUTE; 0 disables. Keys on the client IP from the shared trusted-proxy walk (#766): the SOCKET PEER when TRUSTED_PROXIES is unset, or the Forwarded/X-Forwarded-For-derived client when the peer is a listed proxy

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 craig_common::client_ip. When the immediate TCP peer is inside a listed CIDR, the forwarding chain is walked right-to-left past trusted hops to the real client; untrusted peers' headers are spoofable and ignored. An invalid CIDR FAILS STARTUP. Unset = every request behind a proxy shares that proxy’s one bucket, so list your ingress here in fronted deployments

DB_MAX_CONNECTIONS

u32

10

No

Database connection pool size. Size against the server’s max_connections per the deployment guide § Database connection budget (#1160) — detached advisory-lease sessions sit outside this pool’s accounting

DB_IDLE_TIMEOUT_SECS

u64

600

No

Database connection idle timeout in seconds

DB_STATEMENT_TIMEOUT_MS

u64

30000

No

F-019: Postgres statement_timeout set on every acquired connection (floor 10,000 ms — ADR-068’s HTTP tx funnels SET LOCAL a 10s budget, which would silently RAISE a tighter session bound, so load refuses below the floor; top clamp 3,600,000 ms). Batch jobs extend per-transaction via SET LOCAL; the pool’s 5 s acquire timeout is a separate FIXED constant, deliberately not a knob (#1160)

ADMIN_UNIT_LABEL

String

 — 

Yes

Display label for the admin-unit concept (County, Region, Chapter, etc.; Plan A §12 removed the silent County default)

AUTHZ_REQUIRE_FULL_COVERAGE

bool

false

No

Boot-bail when the jurisdiction×resource-type authz matrix has uncovered tuples

AUTHZ_POLICY_TTL_SECONDS

u64

3600

No

Authz ruleset-cache TTL fallback for missed invalidation events

AUTHZ_EVAL_TIMEOUT_MS

u64

5000

No

Per-authz-decision eval budget; elapsing returns a typed 503 (#1126). Valid 1..=19000 since ADR-068: the typed 503 must fire before the 20s Standard request ceiling

AUTHZ_FUNCTION_TIMEOUT_MS

u64

min(5000, AUTHZ_EVAL_TIMEOUT_MS)

No

JS function-node interrupt bound published into zen’s PROCESS-GLOBAL config at boot (#1328); explicit values must sit in 1..=AUTHZ_EVAL_TIMEOUT_MS. craig-rules validates but never applies it — CRAIG_RULES__FUNCTION_TIMEOUT_MS (#1046) owns that process’s bound

EVENT_OUTBOX_RETENTION_DAYS

u32

30

No

#965/#1129 (ADR-058): event_outbox prune window — PUBLISHED rows only; pending rows are never age-deleted. 0 disables the outbox sweep. Also bounds the admin replay endpoint and the exchange/financial local sweeps. Change fleet-wide only

EVENT_INBOX_RETENTION_DAYS

u32

31

No

#965/#1129 (ADR-058): event_inbox prune window. Load-validated >= outbox + 1 when both non-zero (the dedup-horizon margin) and ⇐ 3650; 0 disables the inbox sweep. Change fleet-wide only

RETENTION_ARCHIVE__ENABLED

bool

false

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 /archive/run+/archive/purge (typed 403 naming this knob). See the deployment guide’s Retention & Archive runbook before enabling

RETENTION_ARCHIVE__HOT_WINDOW_DAYS

u32

90

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

3600

No

#1129: scheduled archive-pass cadence; 0 = no worker (manual endpoints still work when enabled)

RETENTION_ARCHIVE__BATCH_SIZE

u32

1000

No

#1129: rows per archive batch (1..=10000); one batch = one object + one crash-atomic prune transaction

RETENTION_ARCHIVE__MAX_BATCH_BYTES

usize

33554432 (32 MiB)

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

http://localhost:8001

craig-rules base URL, read per-service (CRAIG_<SVC>__RULES_ENGINE_URL) for the authz RulesetSource; craig-cases additionally uses it for the evaluate relay + §G4 auto-link consumer. craig-rules itself boots authz from its own DB and reads no URL

craig-rules

EVAL_TIMEOUT_MS

5000

Budget for one rules evaluation dispatch (queue wait + evaluation), milliseconds; elapsing returns 503 on POST /v1/rules/evaluate and fails the domain-event handler (#784). Valid 1..=19000 — validated at boot; since ADR-068 the cap is the 20s Standard request ceiling − 1s, so the typed eval 503 always beats the generic ceiling 504. Successful-call latency ≈ budget + audit write (the audit tx runs after the dispatch, bounded by DB_STATEMENT_TIMEOUT_MS).

craig-rules

FUNCTION_TIMEOUT_MS

min(5000, eval budget)

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 1..=EVAL_TIMEOUT_MS (validated at boot): a JS node outliving the dispatch budget turns a clean typed runtime_error audit row into a caller timeout. Per-node, not per-dispatch — a multi-JS-node graph can legitimately sum past it; the eval budget stays the caller ceiling. Process-global: also governs the embedded authz engine’s JS nodes in the craig-rules process (fleet mirror: #1328). v1 (string-content) function nodes carry zen’s hard 500ms bound regardless.

craig-rules

DECISION_REFRESH_SECONDS

300

Decision-cache sweep interval, seconds (#1188 / the ADR-006 #1188 amendment). Valid 10..=86400 — validated at boot. Each instance periodically reconciles its compiled-decision cache against the DB via the identity-aware (id, revision) CAS; bounds staleness after a LOST rules.cache_invalidated event to ≤ ~2×interval + pass duration under a healthy DB (≈10 min at the default). Replicas de-phase via a deterministic per-instance start offset; devstack compose pins 15 so the fault e2e observes convergence.

craig-cases

ENCRYPTION_MODE

required

Field-encryption posture (ADR-020 / ADR-048): required (default — boot fails without a valid key) or optional (keyless ⇒ plaintext; unit-test / non-PII deployments only). Any other value is a hard startup error. The field parses fleet-wide (CRAIG_<SVC>__ENCRYPTION_MODE) but only cases consults it. The devstack runs required since epic &66 C7.

craig-cases

CRAIG_FIELD_ENCRYPTION_KEY (no service prefix)

 — 

32-byte base64 field key, shared with craig-seed so seeded rows match service-written ones. In required mode any load failure (absent, malformed, wrong-length) fails boot. In optional mode any load failure — including a set-but-malformed key — silently disables encryption (keyless plaintext); this is deliberately unlike craig-seed’s loader, which is fatal on a malformed key in both modes (ADR-048 C5 amendment). At boot, cases additionally verifies the loaded key against the crypto_key_lineage anchor (KCV + canary, ADR-048 §D3) and refuses startup on key↔data mismatch, naming the remedy — so a wrong-but-well-formed key (or a keyless boot over keyed data) cannot serve requests.

craig-cases

JWS_REPLAY_TTL_HOURS

24

TTL (hours) for jws_seen_jti replay-defense rows (mandated-reporter signing); rows older than this are pruned and their jti becomes acceptable again

craig-cases

JWS_REPLAY_CLEANUP_INTERVAL_SECONDS

3600

Poll interval for the jws_seen_jti cleanup worker

craig-exchange

ICPC_DEADLINE_DAYS

84

ICPC home-study deadline in calendar days (only consumed by the ICPC request update path)

craig-financial

CASES_URL

http://localhost:8002

craig-cases URL — placement-activation billing resolves the child’s DOB from cases at event time (#979)

craig-financial

PLACEMENT_BASE_URL

http://localhost:8003

#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

calendar_month

Payment-period boundary policy for per-diem billing rollups: calendar_month | fiscal_month | iso_week (Plan A §13)

craig-financial

SUBSIDY_GENERATOR_INTERVAL_SECONDS

0

#1068/ADR-053: monthly subsidy generation scheduler interval. 0 = disabled — enabling automated money generation is explicit deployment consent. Also requires JURISDICTION=georgia (policy is Georgia-defined only). Devstack/CI set 3600. The first tick runs immediately on boot; the deployment-wide run lease (pg_try_advisory_lock) keeps multi-replica profiles single-writer

craig-financial

SUBSIDY_REVIEW_SWEEP__INTERVAL_SECONDS

0

#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 JURISDICTION=georgia. Interval alone (modes off) is a pure observe cadence: one run row + one completion event per tick, backlog gauges, zero agreement writes. The knob group nests on the separator (CRAIG_FINANCIALSUBSIDY_REVIEW_SWEEP__…); enablement demonstrated by docker-compose.sweep-demo.yml

craig-financial

SUBSIDY_REVIEW_SWEEP__MATERIALIZE

false

#1096: scheduled sweeps materialize missing first review slots from term anchors

craig-financial

SUBSIDY_REVIEW_SWEEP__AUTO_SUSPEND

false

#1096: enforce leg 1 — suspend agreements whose open review is past due + 30-day grace; recorded approval_level = "system" (the knob IS the authority, ADR-054)

craig-financial

SUBSIDY_REVIEW_SWEEP__AUTO_TERMINATE

false

#1096: enforce leg 2 — terminate agreements suspended three months or more (three_months_suspended)

craig-financial

SUBSIDY_REVIEW_SWEEP__AUTO_PER_DIEM_HANDOFF

false

#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 per_diem_begins, truth-dated to the per-diem start). Detection and the backlog counter run every sweep regardless of this knob; it consents ONLY the scheduled termination — the manual preview/execute path always enforces the pinned legs under acknowledged counts

craig-financial

SUBSIDY_ERR__ENABLED

false

#1069 M2 (F4): the one-shot ERR creation gate, nested as CRAIG_FINANCIALSUBSIDY_ERRENABLED. Default OFF — the ⁂ money policies (proration formula, 120-day clock, kinship set, −12 backdating floor) are best readings pending DFCS confirmation; enabling is the operator’s RECORDED consent to run on them. Off ⇒ the create endpoint refuses with a typed 403 naming the knob. Devstack/e2e set true

craig-financial

SUBSIDY_SG__ENABLED

false

#1070 (U4): the SG/NRSG guardianship ENROLLMENT-consent gate, nested as CRAIG_FINANCIALSUBSIDY_SGENABLED. Default OFF — one knob covers the family; the ⁂ readings (6-month residence floor, strict pre-transfer signing, the payments-begin boundary incl. the TANF shift, approval-anchored review dues, the 1st-of-month gap month) are best readings pending DFCS confirmation; enabling is the operator’s RECORDED consent. Gates NEW enrollments (create + activate) ONLY — existing agreements' lifecycle (generator, ALL transitions incl. the corrective guardianship_finalized, reviews, sweep) is deliberately grandfathered, so a knob-off flip never strands owed money. Devstack/e2e set true

craig-financial

SUBSIDY_IMPORT__ENABLED

false

#1071/ADR-057 (D12): the closed-cohort conversion-import gate, nested as CRAIG_FINANCIALSUBSIDY_IMPORTENABLED. Default OFF — flipped true only for the conversion WINDOW (the deployment guide’s cutover runbook), which is the operator’s recorded consent to stage + finalize SHINES history. Gates every import WRITE (create-batch, stage, abort, and finalize — the money-consent act); GETs stay readable knob-off for post-conversion audit, and the lifecycle of already-MATERIALIZED agreements (generator, transitions, reviews, sweep) never consults the knob — disabling after the window strands nothing

craig-reporting

CASES_URL

http://localhost:8002

craig-cases URL — the NCANDS generate path fetches export rows from cases at request time (#652 Phase D)

craig-security

DETECTION_INTERVAL_SECS

60

Periodic detection-scan interval, seconds (note the _SECS suffix). 0 = scheduler disabled (the devstack default since #1167 — on-demand POST /v1/security/detection/run still works); unset/unparseable falls back to 60

craig-composition

RULESETS_DIR

rulesets

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=shines to a SHINES instance + keyring sidecar (ADR-042)

Variable Type Default Required Description

PORT

u16

 — 

Yes

Service listening port

MODE

IntakeMode

integrated

No

Operating mode: integrated or standalone

BACKEND_PROFILE

BackendProfile

none

No

ADR-042 durable-backend selection: none (forwards to craig-cases) or shines (standalone only; rejected in integrated mode)

OIDC_ISSUER

String

 — 

Yes

OIDC issuer URL (both modes — intake mints client_credentials service tokens for its outbound S2S calls)

OIDC_INTERNAL_URL

String

 — 

No

Internal OIDC URL for split-DNS deployments (falls back to issuer)

CLIENT_ID

String

 — 

Yes

OIDC client_credentials principal (both modes; the S2S bearer for cases-forward + security lookups)

CLIENT_SECRET

String

 — 

Yes

Secret for the service principal

LOG_LEVEL

String

info

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 (* is by-design for public intake forms; validated at boot)

BODY_LIMIT

usize

11534336 (11 MiB)

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 BACKEND_PROFILE=shines only)

KEYRING_URL

String

 — 

SHINES

craig-intake-keyring sidecar base URL (standalone SHINES only; signer-key lookup + JWS replay dedup, ADR-042 §D8)

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 received_ip_hash (Plan B F-003) — without it stored hashes are brute-forceable over the IPv4 space

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

disabled

No

CAPTCHA verification secret (set to disabled to skip)

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

false

No

Production guard — when true, boot fails if CAPTCHA_SECRET is still disabled

PUBLIC_RATE_LIMIT

u32

5

No

Maximum public submissions per IP per hour

SIGNED_RATE_LIMIT

u32

60

No

Requests per IP per hour on the standalone-SHINES signed nest /signed/v1 (machine API; ADR-042 §D9)

SIGNED_MAX_FILE_BYTES

usize

10485760 (10 MiB)

No

Per-file cap for signed-path multipart attachments (ADR-042 §D9)

SIGNED_BODY_LIMIT

usize

31457280 (30 MiB)

No

Whole-request body cap for the signed nest (overrides BODY_LIMIT on those routes)

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

false

No

ADR-046 dev/test-only ephemeral self-signed cert; requires the self-signed-tls build feature (fail-closed without it). NEVER in production

DEBUG_EMIT_CPS_REQUEST

String

 — 

No

#713 dev/integration only — debug CpsRequest panel gate. Active ONLY when set to the exact value EXPOSE_SSN_PII (a bare true does NOT enable it); requires BACKEND_PROFILE=shines. When active, the submit response echoes the exact conformed SHINES CpsRequest (third-party / minor PII, incl. SSNs) with Cache-Control: no-store. NEVER enable in production.

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); 0 is rejected by boot validation

LOG_LEVEL

String

info

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

86400 (24 h)

No

JWS replay-record retention: records with iat older than this are pruned and their jti becomes acceptable again. Keep well beyond intake’s own JWS freshness window

MAX_KEYS

usize

10000

No

Cap on stored signer keys; register returns 429 at capacity (bounds the unauthenticated-register disk-growth surface)

ENABLE_KEY_LIST

bool

false

No

#714: mounts the metadata-only GET /keys view. Off ⇒ route absent (404). Exposes kid/display name/status/timestamps only — never the JWK or holder identifier

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., craig-ui)

KEYCLOAK_REDIRECT_URI

String

 — 

Yes

OAuth redirect URI

CLIENT_ID

String

 — 

No

craig-web’s OWN client_credentials service principal for outbound S2S calls (Plan E §9 / ADR-028) — distinct from KEYCLOAK_CLIENT_ID. Unset/empty ⇒ warn + fall back to forwarding the worker bearer

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 (CRAIG_WEB__ prefix; same pair semantics as the shared table) — this is what mints the X-Craig-Actor header the fleet evaluates workers by

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 (tower-cookies::Key::from(bytes)). Must be at least 64 bytes — the service fails to start otherwise. Generated via openssl rand -hex 32 (64 hex chars = 64 bytes). See ADR-013.

SESSION_MAX_AGE_SECS

i64

1800

No

Session cookie lifetime in seconds (default: 30 minutes). Increase for longer idle timeouts; decrease for stricter security.

SESSION_SECURE

bool

true

No

Emit the Secure flag on the session cookie. Set to false only for plain-HTTP dev loops.

LOG_LEVEL

String

info

No

Logging level

DEFAULT_LOCALE

String

en

No

Default i18n locale. Only en is supported: craig-web ships en worker catalogs only and pins locale negotiation to this default (#1332), so a non-en value renders raw message keys across the worker UI (the service warns at boot). Real es worker catalogs + a switcher are tracked in #1487

THEME

String

 — 

Yes

UI theme name (CSS theme identifier; Plan A §12 removed the silent georgia-orchard default)

BRANDING_AGENCY

String

 — 

Yes

Agency name for branding (Plan A §12 removed the silent default)

BRANDING_APP_NAME

String

CRAIG

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

11534336 (11 MiB)

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 BODY_LIMIT (see note).

A file upload traverses up to four request-body caps — ingress/reverse-proxy → BFF (CRAIG_WEBUPLOAD_BODY_LIMIT, 11 MiB default) → storing backend (CRAIG_CASESBODY_LIMIT / CRAIG_PLACEMENTBODY_LIMIT; craig_common default 2 MiB) → object store (CRAIG_STOREMAX_UPLOAD_BYTES, 50 MiB default). The smallest wins, so a large attachment 413s at whichever tier is under-sized. A deployment accepting large attachments MUST size every tier to at least the intended maximum (the conservative 2 MiB backend default in particular). See the "Attachment upload size — configure every tier" note in Deployment Guide. (Whether attachment-accepting backends should scope a larger limit to just their attachment routes, rather than a global per-service raise, is tracked as a follow-up.)

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

local

No

Backend type: local (filesystem) or s3 (S3-compatible). Any other value fails deserialization

BUCKET

String

 — 

S3

S3 bucket name (enforced when the S3 client is built, not at config load)

LOCAL_ROOT

String

/tmp/craig-store

No

Root directory for local filesystem backend (created on startup)

S3_ENDPOINT

String

 — 

S3

S3 endpoint URL (e.g., http://garage:3900)

S3_REGION

String

us-east-1

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

52428800 (50 MiB)

No

Maximum upload file size. Boot-validated ≥ RETENTION_ARCHIVE__MAX_BATCH_BYTES by the retention engine

Path-style addressing and allow_http are HARDCODED (always path-style, plain HTTP permitted) — there are no TLS/addressing knobs; front the endpoint with TLS at the network layer if required.

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 --jurisdiction flag). Unset/empty/unknown/multiple ⇒ hard error — same activation contract as the services (see Deployment-Global Variables)

CRAIG_SEED__ENCRYPTION_MODE

String

optional (when unset)

No

Accepts only optional or required — unset is treated as optional, and any other value (typo, empty, non-Unicode) is a hard error (fail-closed, never a silent fallback). required fails if no key is present.

CRAIG_FIELD_ENCRYPTION_KEY

String

 — 

In required mode

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).

Edit this page · latest