Architecture
On this page
CRAIG follows a modular, service-oriented architecture modeled after OpenStack. Like OpenStack, all functionality is accessible through three equivalent interaction modes, so no capability is exclusive to any one client:
-
REST APIs: each functional domain (case management, eligibility, data exchange, etc.) is exposed as an independent, versioned REST service. The APIs are the authoritative interface and the foundation for all other clients.
-
CLI (
craig): a first-class command-line client providing full access to all API functionality, suitable for scripting, automation, and administrative workflows. -
Web UI: a browser-based interface for caseworkers, constituents, and providers, built entirely on the public REST APIs.
-
Message Bus (RabbitMQ): asynchronous event-driven communication between services, for data exchange, notifications, and audit events.
-
Rules and Policy Engine (GoRules / Zen Engine): jurisdiction-configurable policy rules evaluated at runtime using JSON Decision Models (JDM). Title IV-E eligibility, safety and risk assessment, timeliness monitoring, and other policy logic are defined as portable rule sets that each jurisdiction loads without modifying core code, which directly satisfies the 45 CFR § 1355.53(a)(1) requirement to separate business rules from core programming.
-
Modular Design: components can be independently developed, deployed, replaced, or shared with other states and tribes, per 45 CFR § 1355.53(a)(1).
-
Plain Language Documentation: all automated functions are documented in non-technical language, per 45 CFR § 1355.53(a)(2).
Service Architecture
Three fan-outs are drawn at the subgraph level, or described in prose, to keep the graph legible (#1418); no information is lost:
-
OIDC: the identity provider authenticates every core service (the single
AUTH → COREedge). Keycloak is the reference backend, not a CRAIG service itself; see the IdP integration guide. -
Message bus: every API service publishes and consumes domain events over RabbitMQ, with two exceptions.
craig-webis a pure BFF with no bus connection at all; it reads exclusively through service APIs.craig-intake, the stateless edge, holds no MQ connection either; it forwards over HTTP only (ADR-017).craig-security’s dashed edge is the informative special case: the wildcard audit subscriber (routing key `#) described under Cross-cutting Service Contracts below. -
Databases: each core service owns its like-named PostgreSQL database (
craig_rules,craig_cases, and so on), schema-per-service, with no cross-service DB access. The canonical service-to-port-to-database table is in Services.craig-intakeis the stateless exception (ADR-017);craig-webis DB-less.
Cross-cutting Service Contracts
Every CRAIG service except the pure-BFF craig-web honors the cross-cutting contracts below, enforced by shared middleware and helper crates. Since #1186 (ADR-061) that includes worker supervision: every background worker registers on a per-process Supervisor under an expected-worker manifest. A Critical worker’s death fail-fasts the process, with bounded drains and a nonzero exit, and /readyz gates on the worker registry + the RabbitMQ parent connection; a knob-disabled worker shows up as Disabled on /healthz.
| Contract | Implementation |
|---|---|
OIDC authentication |
|
Record-level authorization |
|
RFC 9457 Problem Details |
|
Request idempotency (ADR-062 §B) |
|
Shared |
|
Stateless session cookies (craig-web only) |
|
Wildcard audit subscriber |
|
Data Exchange Topology
| All exchanges use a single jurisdiction-defined data exchange standard per 45 CFR § 1355.52(f). |
Reference Data Architecture
CRAIG uses a two-tier approach to reference data:
-
Tier 1: Compile-time (
craig-referencecrate). Domain enums (Gender, Race, PlacementType, etc.), FIPS codes for all US states and administrative units, and AFCARS/NCANDS field translations are defined as Rust enums with strum-derived conversions. These are shared across the workspace and validated at compile time. The crate includes 48 unit tests. -
Tier 2: Runtime (Admin Unit Registry). Deployment-specific administrative units, such as counties, regions, boroughs, chapters, districts, and municipalities, are managed through CRUD endpoints on
craig-security. Default seeds for all 57 US jurisdictions (50 states, DC, 5 inhabited territories, and the Minor Outlying Islands; 3,235 county-equivalents sourced from the Census Bureau’snational_county2020.txt) are compiled directly intocraig-reference::counties, with no on-disk CSV layer involved.cargo xtask seed-admin-units [--only <jurisdiction_key>]performs an idempotent UPSERT keyed on(name, jurisdiction). A deployment config endpoint,GET /v1/security/admin/config, returns theadmin_unit_label(e.g., "County" for Georgia, "Region" for Texas), so the UI can adapt its labels without code changes. That label itself comes from the requiredCRAIG_<SVC>__ADMIN_UNIT_LABELenv var; there is no in-code default.
Post-deployment extensions, such as custom non-FIPS units like county consortia or experimental tribal compacts, go through the same craig-security::admin_unit_registry CRUD endpoints. That’s the operator extension mechanism.
Worker Identity
All API handlers identify the acting worker by claims.sub — the OIDC issuer’s stable user UUID. The web UI continues to display preferred_username for human readability. craig-security lazy-populates the worker_identities table from JWT claims on first authenticated request — no IdP admin-API dependency; this works across any OIDC backend. Devstack test users have pinned UUIDs in devstack/keycloak/craig-realm.json so seed data and integration tests can reference them deterministically.
Multi-Jurisdictional Posture
CRAIG ships jurisdiction-neutral by default: onboarding a new state, tribe, or county requires only data, not code.
-
Domain rules: drop JDM JSON files into
rulesets/<jurisdiction>/<jurisdiction>-<function>.json(e.g.georgia-ive-eligibility.json). The rules engine evaluates them following the{jurisdiction}-{function}naming convention. -
Authorization policies: drop JDM JSON files into
rulesets/<jurisdiction>/<jurisdiction>-authz-<resource>.json. The authz engine (craig-authz) consumes the same naming convention. Roles, scoping, approval workflows are inputs to the decision tables. -
Admin units:
cargo xtask seed-admin-units --only <jurisdiction_key>upserts the compiled-in dataset, any of the 57 US jurisdictions, intoadmin_unit_registry. For deployment-specific units, such as county consortia, tribal compacts, or multi-state regional districts, operators use thecraig-security::admin_unit_registryCRUD endpoints instead: the same flow, but at runtime, so it doesn’t require redeployment. -
Env vars: every service requires
CRAIG_<SVC>JURISDICTION=<jurisdiction>andCRAIG_<SVC>ADMIN_UNIT_LABEL=<label>; craig-web also requiresCRAIG_WEBTHEMEandCRAIG_WEBBRANDING_AGENCY. There’s no silent fallback to Georgia. -
OIDC identity: any compliant issuer works, since OIDC discovery resolves endpoints at boot. Worker identities populate from JWT claims.
See the archived Multi-Jurisdictional Authorization plan for the underlying architectural decisions (14 step MRs, 5 ADRs, May 2026).
Plugin Runtime
Server-side UI plugins extend the craig-web BFF without modifying it (Plan W; ADR-033). A plugin is a crate under plugins/ whose sync-pure render fn is annotated with #[craig_plugin(slug, manifest)] (craig-plugin-macros). That annotation emits a linkme CRAIG_PLUGINS registration carrying the plugin’s Plugin.toml manifest and its render fn. The host owns all I/O; the plugin only renders.
-
Boot:
craig-webmaterializes a validatedPluginRegistry(craig-plugin-contracts) from the slice, failing fast on a duplicate slug, an unknownrequired_role, or adisplay_name{term.*}key that doesn’t resolve against the materialized terminology. -
Render:
GET /plugins/<slug>, insideprotected_routes, host-fetches the manifest-declared[data]endpoint. That fetch is SSRF-guarded by a deny-by-default host allow-list, per theauthmode (none/service_token/user_jwt), and maps the response to aFetchOutcome. The plugin’s render fn is then called and returns a four-state (data/empty/error) CSP-clean HTML fragment;loadingis the shell’s own htmx placeholder. -
Opt-in: plugins compile into
craig-webbehind per-plugin Cargo features (a#[cfg(feature = "…")] use … as _;linkmeforce-link), so a deployment composes its own plugin set. Production omits the reference plugin. -
Source-agnostic seam: the
PluginSourcetrait stays stable across backends. v1 is the compile-timelinkmeslice; v2 is WASM, with no contract change, since every value crossing the plugin boundary isserde-serializable.
The full design is in Plan W — Plugin Manifest + Render Runtime. The composition engine that arranges plugins into surfaces is Plan X (ADR-035).