All persistent state in Caracal lives in a single Postgres database. Thirty tables cover the control plane (zones, applications, policies, resources, grants), the runtime plane (sessions, delegation edges, step-up challenges), the audit ledger, and the operational outbox. Schema changes are applied via numbered migration files in infra/postgres/migrations/.
Table Purpose zonesTenant boundaries. Each zone has its own signing key, policy set, and session namespace. applicationsOAuth clients registered in a zone. Carries credential type, traits, and DCR settings. resourcesProtected targets. Each resource has an identifier, upstream URL, scope list, and optional credential provider. providersCredential providers (OAuth2, OIDC, API key, workload). Sensitive fields encrypted under the zone KEK. policiesRego policy source. Immutable after creation; each policy has an associated policy_versions row. policy_versionsImmutable versioned snapshots of policy source, identified by SHA-256 content hash. policy_setsNamed groupings of policies. One policy set per zone may be active at a time. policy_set_versionsImmutable versioned snapshots of a policy set manifest. policy_set_bindingsMaps a zone to its active (and optional shadow) policy set version. One active binding per zone. secretsEncrypted key material. Stores zone signing keys (PEM private key, encrypted with ChaCha20-Poly1305 under ZONE_KEK). invitationsZone member invitations (email-based, expiring). teamsTeam groupings within a zone. admin_tokensHashed API admin tokens for the control plane. application_dependenciesApplication-to-resource dependency declarations.
Table Purpose sessionsUser and application sessions. Tracks subject, status, expiry, and claims snapshot. agent_sessionsAgent execution sessions. Tracks depth in tree, child count, capabilities, TTL, and status. agent_topologyParent-child relationships for the agent tree. Enforces MAX_CHILDREN. delegation_edgesDirected authority transfers. Tracks status, scope, constraints, expiry, and edge version. delegation_graph_epochsMonotonic epoch counter per zone. Incremented on every edge create or revoke. step_up_challengesActive step-up challenges. Tracks secret hash, resource set hash, satisfaction, and consumption. delegated_grantsBrokered third-party grant tokens (OAuth flow state). agent_servicesAgent service registry. Maps application IDs to endpoint URLs and framework metadata. agent_invocationsDurable invocation tracking. Supports idempotency, retry policy, and deadline enforcement. resource_rate_limitsPer-resource rate limit state.
Table Purpose audit_eventsAppend-only HMAC-chained record of every OPA evaluation. Partitioned by occurred_at. admin_audit_eventsAdministrative action audit log (control plane operations). audit_export_watermarkTracks Parquet export progress (offset into the audit_events table). audit_ingest_alertsRecords tamper detection findings from the Audit service’s chain validation.
Table Purpose caracal_outboxTransactional event outbox. Events are written here in the same transaction as the state change, then published to Redis streams by a background job. gateway_resource_bindingsCached resource-to-upstream bindings for the Gateway’s in-memory binding store.
audit_events is partitioned by RANGE on occurred_at. Monthly partitions are pre-created:
CREATE TABLE audit_events_y2026m05
PARTITION OF audit_events
FOR VALUES FROM ( ' 2026-05-01 ' ) TO ( ' 2026-06-01 ' );
The Audit service’s retention job drops partitions older than AUDIT_RETENTION_DAYS (default 365). Pre-creating forward partitions prevents INSERT failures when a partition does not yet exist for the current month.
RLS is enabled on all 22 zone-scoped tables. The policy on each table is:
CREATE POLICY zone_isolation ON { table }
USING (zone_id = current_setting( ' caracal.zone_id ' , true)
OR current_setting( ' caracal.zone_id ' , true) IS NULL
OR current_setting( ' caracal.zone_id ' , true) = '' );
When a connection sets SET LOCAL caracal.zone_id = '{zone_id}' at the start of a transaction, the database filters every query to that zone automatically. When the setting is absent or empty, all rows are visible (used by admin and maintenance operations). This provides a defense-in-depth layer: even if application code omits a WHERE zone_id = ? clause, the database enforces isolation.
Tables with RLS: providers, applications, sessions, secrets, delegated_grants, policies, policy_sets, policy_set_bindings, resources, audit_events, agent_sessions, invitations, teams, delegation_edges, agent_services, agent_invocations, gateway_resource_bindings, resource_rate_limits, step_up_challenges, admin_audit_events, admin_tokens, delegation_graph_epochs.
Each service connects with a Postgres role scoped to the minimum permissions it requires.
Tables Permissions zones, applications, providers, resources, application_dependencies, policies, policy_versions, policy_sets, policy_set_versions, policy_set_bindings, agent_servicesSELECT sessions, delegated_grants, secrets, step_up_challenges, agent_sessions, agent_topology, delegation_edgesSELECT, INSERT, UPDATE
The STS reads zone configuration and policies. It writes sessions and step-up challenges as exchanges proceed. Audit events flow through the Redis stream, not direct DB writes.
Tables Permissions zones, applications, providers, resources, application_dependencies, policies, policy_sets, delegated_grants, secrets, invitations, teams, agent_services, caracal_outbox, delegation_graph_epochsSELECT, INSERT, UPDATE, DELETE policy_versions, policy_set_versionsSELECT, INSERT (immutable; never deleted) policy_set_bindingsSELECT, INSERT, UPDATE, DELETE
The API is the control plane and needs broad write access. It cannot read audit tables.
Tables Permissions audit_events, audit_export_watermark, audit_ingest_alertsSELECT, INSERT
The Audit service has no UPDATE or DELETE on audit_events. This is enforced at the database layer, not just in application code. The append-only guarantee is a database constraint.
Tables Permissions agent_sessions, agent_topology, agent_invocations, agent_services, caracal_outbox, delegation_graph_epochs, delegation_edgesSELECT, INSERT, UPDATE zones, applicationsSELECT
The Coordinator manages the agent graph and invocations. It does not touch policy, audit, or credential tables.
Tables Permissions zones, applications, resources, providers, gateway_resource_bindingsSELECT
The Gateway is read-only. It fetches resource bindings to resolve upstream URLs and credential provider configuration. It writes nothing to Postgres.
Column Type Notes idTEXT UUID primary key zone_idTEXT Zone reference (partition-scoped) event_typeTEXT e.g., "token_exchange", "jti_collision" request_idTEXT Trace ID from the exchange request decisionTEXT "allow" or "deny"policy_set_idTEXT Policy set that was active policy_set_version_idTEXT Specific version that evaluated manifest_shaTEXT SHA-256 of the policy set version manifest evaluation_statusTEXT "complete" or other OPA statusdetermining_policies_jsonJSONB Policies that drove the decision diagnostics_jsonJSONB Policy-returned diagnostic metadata metadata_jsonJSONB Additional context (principal, resource, session IDs) occurred_atTIMESTAMPTZ Partition key; microsecond precision ingested_atTIMESTAMPTZ When the Audit service wrote the row content_sha256BYTEA SHA-256 of all event fields (chain anchor) prev_content_sha256BYTEA SHA-256 of the previous event in sequence chain_hmacBYTEA HMAC-SHA256 linking this event to its predecessor chain_seqBIGINT Monotonic sequence number within the zone ingest_signatureBYTEA Audit service’s own signature over the row
Column Type Notes idTEXT UUID primary key zone_idTEXT Zone reference source_session_idTEXT Delegating agent session target_session_idTEXT Agent session receiving authority issuer_application_idTEXT Application owning the source session receiver_application_idTEXT Application owning the target session resource_idTEXT (nullable) If set, restricts authority to this resource scopesTEXT[] Scopes the target may request constraints_jsonJSONB Caveats (ttl_seconds, max_hops, budget, policy_approved) statusTEXT "active", "revoked", or "expired"expires_atTIMESTAMPTZ Edge expiry edge_versionINT Incremented on every status change revoked_atTIMESTAMPTZ When the edge was revoked (nullable)
Column Type Notes idTEXT UUID primary key producerTEXT "api" or "coordinator"topicTEXT Target Redis stream name dedupe_keyTEXT Idempotency key (unique per producer + topic) payload_jsonJSONB Stream message payload statusTEXT "pending", "published", or "dead"attemptsINT Retry counter available_atTIMESTAMPTZ Next eligible publish time (backoff delay) published_atTIMESTAMPTZ Set on successful publish
Column Type Notes idTEXT UUID primary key zone_idTEXT Zone reference session_idTEXT Agent session that triggered the challenge principal_idTEXT Application or user ID of the requester challenge_typeTEXT "mfa", "human_approval", or "software_attestation"challenge_secret_hashBYTEA SHA-256 of the raw secret returned to the client resource_set_hashBYTEA SHA-256 of the canonical resource list (binds challenge to request) metadata_jsonJSONB Additional context expires_atTIMESTAMPTZ Challenge expiry (5 minutes from creation) satisfied_atTIMESTAMPTZ Set by the external system via the satisfaction API consumed_atTIMESTAMPTZ Set atomically by the STS when the challenge is used in an exchange retry