Skip to content

Storage Model

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

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

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

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

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

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

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


ColumnTypeNotes
idTEXTUUID primary key
zone_idTEXTZone reference (partition-scoped)
event_typeTEXTe.g., "token_exchange", "jti_collision"
request_idTEXTTrace ID from the exchange request
decisionTEXT"allow" or "deny"
policy_set_idTEXTPolicy set that was active
policy_set_version_idTEXTSpecific version that evaluated
manifest_shaTEXTSHA-256 of the policy set version manifest
evaluation_statusTEXT"complete" or other OPA status
determining_policies_jsonJSONBPolicies that drove the decision
diagnostics_jsonJSONBPolicy-returned diagnostic metadata
metadata_jsonJSONBAdditional context (principal, resource, session IDs)
occurred_atTIMESTAMPTZPartition key; microsecond precision
ingested_atTIMESTAMPTZWhen the Audit service wrote the row
content_sha256BYTEASHA-256 of all event fields (chain anchor)
prev_content_sha256BYTEASHA-256 of the previous event in sequence
chain_hmacBYTEAHMAC-SHA256 linking this event to its predecessor
chain_seqBIGINTMonotonic sequence number within the zone
ingest_signatureBYTEAAudit service’s own signature over the row
ColumnTypeNotes
idTEXTUUID primary key
zone_idTEXTZone reference
source_session_idTEXTDelegating agent session
target_session_idTEXTAgent session receiving authority
issuer_application_idTEXTApplication owning the source session
receiver_application_idTEXTApplication owning the target session
resource_idTEXT (nullable)If set, restricts authority to this resource
scopesTEXT[]Scopes the target may request
constraints_jsonJSONBCaveats (ttl_seconds, max_hops, budget, policy_approved)
statusTEXT"active", "revoked", or "expired"
expires_atTIMESTAMPTZEdge expiry
edge_versionINTIncremented on every status change
revoked_atTIMESTAMPTZWhen the edge was revoked (nullable)
ColumnTypeNotes
idTEXTUUID primary key
producerTEXT"api" or "coordinator"
topicTEXTTarget Redis stream name
dedupe_keyTEXTIdempotency key (unique per producer + topic)
payload_jsonJSONBStream message payload
statusTEXT"pending", "published", or "dead"
attemptsINTRetry counter
available_atTIMESTAMPTZNext eligible publish time (backoff delay)
published_atTIMESTAMPTZSet on successful publish
ColumnTypeNotes
idTEXTUUID primary key
zone_idTEXTZone reference
session_idTEXTAgent session that triggered the challenge
principal_idTEXTApplication or user ID of the requester
challenge_typeTEXT"mfa", "human_approval", or "software_attestation"
challenge_secret_hashBYTEASHA-256 of the raw secret returned to the client
resource_set_hashBYTEASHA-256 of the canonical resource list (binds challenge to request)
metadata_jsonJSONBAdditional context
expires_atTIMESTAMPTZChallenge expiry (5 minutes from creation)
satisfied_atTIMESTAMPTZSet by the external system via the satisfaction API
consumed_atTIMESTAMPTZSet atomically by the STS when the challenge is used in an exchange retry