Skip to main content

Database schema

Generated from the migrations in crates/sp-store/migrations by the schema_doc test of sp-store; do not edit. Schema strataplane.

The model has two halves: the spec (what should be: immutable, content-addressed revisions edited through change sets, docs/spec-format.md) and state (what is: hosts, agents, artifacts, rollouts, secrets, audit), written only by the server. Targets PostgreSQL >= 15, no extensions. Embedded and external Postgres use the same schema.

Conventions:

  • ids: uuid (v7, generated by the application) unless noted.
  • tenancy (docs/architecture.md §12): every table with a tenant_id column is tenant data. References between tenant tables are composite foreign keys over (tenant_id, …), so a row can only point into its own tenant; uuid ids stay globally unique, and each such table has UNIQUE (tenant_id, id) as the target. Tables without tenant_id are installation-wide.
  • row-level security: every table with a tenant_id column (except platform_audit_event) has the policy tenant_isolation, enabled and forced, so it is visible only in its tenant's scope: each transaction sets strataplane.tenant (SET LOCAL). Platform scope (strataplane.scope = 'platform') is set only by platform code paths: credential lookup by hash before the tenant is known, the tenants a user can choose at login, controller shard scheduling, tenant administration and deletion, migrations. FORCE applies the policy to the owning role too, so no second database role is needed. RLS backs up the tenant filter of every query; it does not defend against a compromised server.
  • spec revisions: bigint, monotonic per tenant (r1, r2, ...).
  • hashes: bytea (SHA-256, 32 bytes).
  • timestamps: timestamptz.
  • vocabularies: an enum type for lifecycle states, ordered values and values shared by several columns; a text CHECK otherwise.
  • large binaries (journey evidence, wasm modules, validator binaries) live in object storage under keys derived from the row (docs/operations.md §9); the row holds size and hash.

Tables: tenant, tenant_counter, installation, controller_shard, app_user, tenant_member, billing_membership, team, team_member, service_account, role_binding, api_token, web_session, host, host_label, network_zone, spec_blob, spec_revision, spec_object, change_set, change_set_file, change_set_impact, change_set_review, enrollment_token, trust_anchor, agent_instance, class_default, class_served, agent_credential, agent_status_event, agent_log_entry, agent_component, agent_action, artifact, artifact_validation, release, rollout, rollout_stage, rollout_assignment, pin, secret_key, tenant_key, sealed_value, secret_version, secret_delivery, catalog_bundle, plugin_schema, llm_provider, chat, chat_row, audit_event, platform_audit_event, audit_sink, operation, idempotency_key, blob_delete, journey_receipt, journey_run, journey_state, journey_evidence, explore_session, explore_request.

1. Types​

Type tenant_state​

Enum: active, suspended, deleting.

active: normal operation. suspended: no logins and no API tokens; agents keep receiving their current desired artifacts and heartbeats are accepted, but controllers do nothing for the tenant. deleting: every request is rejected and the deletion job removes rows, objects and data keys; the tenant row goes last.

Type role_name​

Enum: viewer, author, approver, operator, secret-admin, admin.

Type host_kind​

Enum: server, client, pod.

Type label_source​

Enum: agent, zone, enrollment, placement, manual.

Label sources in ascending precedence (a higher source wins per label key): agent facts, network zone, enrollment token, placement, manual. Placement labels are the picks of the Placement objects (docs/spec-format.md §3.9), each key belonging to one Placement; they are the picks' only record.

Type agent_kind​

Enum: telegraf, elastic-agent, strataplane-agent, vector.

Type change_set_state​

Enum: draft, in_review, merged, closed.

draft -> in_review -> merged; any state before merged can be closed. Validation runs as an operation and records the digest it validated: it reads the overlay and the digest together under the change set's row lock, which every edit takes, and stores its result only while the change set still has that digest. Submit requires a successful validation of the current digest. An edit or a rebase changes the digest and returns the change set to draft.

Approved is not a state. A change set in review is approved when its requirements are met by approvals at the current digest and no reviewer's latest review at that digest is a reject; a reviewer's latest review at a digest replaces their earlier ones.

Merge requires the digest the caller saw, approval, and a base that is the latest revision. A moved base needs a rebase: a three-way merge per file that lists the paths changed on both sides as conflicts, followed by validation and review of the new digest. Merge recomputes impact and requirements against the current inventory and fails, leaving the change set in review, if the approvals no longer satisfy them.

Type agent_health​

Enum: pending, healthy, degraded, failed, not_reporting.

Health and sync are separate: an agent can be healthy and behind, or in sync and failing. pending = enrolled, never reported; not_reporting = nothing received for 3 expected report intervals (Telegraf and Vector heartbeat.interval, Elastic Agent pollTimeout, strataplane-agent checkin interval); on hosts of kind client it is shown but not alerted on. updating = desired not yet applied, and not yet out_of_sync (never served, or served within the module's reload grace).

Type agent_sync​

Enum: in_sync, updating, out_of_sync, render_error.

Type assurance​

Enum: low, high.

high = bound client cert on the request

Type rollout_state​

Enum: running, paused, halted, completed, aborted.

A rollout starts when it is created (from_revision = the release pointer). The stages are rollout_stage rows, copied from the RolloutPolicy at creation together with its guardrails. running: stages assign agents and bake. paused: an operator stopped it. halted: a guardrail breached after a stage's bake time. Actions: pause; resume (from paused or halted, continuing with the next stage, so resuming a halt accepts the breach); advance (end the current stage's bake time now and evaluate its guardrails); abort. completed moves the release pointer to to_revision. aborted ends all assignments: the rollout's agents fall back to the release pointer, which is still from_revision.

Assignment: a stage, when it starts, takes every agent of the rollout's kind that its selector matches, that no earlier stage took, that no pin holds, and whose artifact at to_revision differs from the one at from_revision. Assignments stay when labels change later. The last stage matches every host (docs/spec-format.md §3.7), so every impacted agent is staged; agents enrolled after it started follow the release pointer. An agent pinned after its assignment follows the pin and leaves its stage's guardrails; a forgotten agent leaves the stage.

Type rollout_stage_state​

Enum: pending, running, passed, breached, skipped.

pending: not started. running: its agents apply to_revision during the bake time. passed / breached: its guardrails held / failed after the bake time. skipped: it took no agents.

Type operation_state​

Enum: queued, running, succeeded, failed.

Type journey_result​

Enum: passed, failed, error.

2. Tables​

tenant​

ColumnTypeNullDefaultDescription
iduuid
slugtextshort name for people and the CLI
display_nametext
statetenant_state'active'::tenant_state
shardintegercontroller shard, fixed at creation (docs/operations.md §3)
limitsjsonb'{}'::jsonbquotas, docs/architecture.md §12
external_idtextyesservice mode: the billing service's workspace id
entitlementjsonbyesservice mode: the newest entitlement billing sent (docs/architecture.md §12)
entitlement_versionbigintyesits version; older pushes are ignored
created_attimestamp with time zonenow()
updated_attimestamp with time zonenow()
state_sincetimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (external_id)
  • UNIQUE (slug)
  • CHECK ((slug ~ '^[a-z0-9]([a-z0-9-]{0,30}[a-z0-9])?$'::text))

tenant_counter​

Per-tenant counters: 'revision', 'change_set', 'rollout', 'audit'. Taking a number locks the row until commit, so numbers of one tenant commit in order and never fork; tenants never wait for each other.

ColumnTypeNullDefaultDescription
tenant_iduuid
nametext
valuebigint0

Constraints:

  • PRIMARY KEY (tenant_id, name)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • CHECK ((name = ANY (ARRAY['revision'::text, 'change_set'::text, 'rollout'::text, 'audit'::text])))

Row-level security (forced): tenant_isolation.

installation​

The installation's own settings, one row, written by strataplane init or the first serve. shards: the controller shard count, fixed here (docs/operations.md §3). backup_at: when a replica last claimed a scheduled backup, so that one replica runs each (docs/operations.md §2).

ColumnTypeNullDefaultDescription
idbooleantrue
shardsinteger
backup_attimestamp with time zoneyes
created_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • CHECK (((shards >= 1) AND (shards <= 1024)))
  • CHECK (id)

controller_shard​

One row per controller shard that ever ran (docs/operations.md §3). The replica holding the shard's lock sets alive_at about once a minute, on the lock connection. Taking the lock sets alive_at and, when alive_at is missing or older than 10 minutes (no replica ran the shard meanwhile), resumed_at = now(); a lock passed on within that time (rolling restart, rebalancing) keeps resumed_at. Agent silence counts from no earlier than the resumed_at of the tenant's shard (agent_instance), so an outage of every replica does not make every agent look silent.

ColumnTypeNullDefaultDescription
shardinteger
alive_attimestamp with time zone
resumed_attimestamp with time zone

Constraints:

  • PRIMARY KEY (shard)
  • CHECK ((shard >= 0))

app_user​

Users are installation-wide: one identity can be a member of several tenants. Platform admins manage tenants and the catalog; they hold no role inside a tenant unless a role binding of that tenant gives them one. A service account is a user of kind 'service' (issuer 'service', subject its id) and a member of exactly one tenant, so every actor column (author, created_by, reviewer, ...) references app_user. Service accounts cannot log in.

ColumnTypeNullDefaultDescription
iduuid
kindtext'human'::text
issuertext'local', 'service' or OIDC issuer URL
subjecttextOIDC sub, local username or service account id
emailtextyes
display_nametext
password_hashtextyesargon2id, local users only
platform_adminbooleanfalse
created_attimestamp with time zonenow()
last_login_attimestamp with time zoneyes
disabled_attimestamp with time zoneyes

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (issuer, subject)
  • CHECK (((kind = 'human'::text) OR ((password_hash IS NULL) AND (NOT platform_admin))))
  • CHECK (((kind = 'service'::text) = (issuer = 'service'::text)))
  • CHECK ((kind = ANY (ARRAY['human'::text, 'service'::text])))

tenant_member​

oidc: created at the first OIDC login (self-hosted installations); local: added by a tenant admin; service: the service account's own membership; billing: pushed by the billing service (service mode, billing_membership) (docs/architecture.md §12). A tenant admin disables the membership, never the user: disabled_at blocks logins and tokens of the user in this tenant only. Every request re-reads the membership, so disabling it also ends the access of open sessions.

ColumnTypeNullDefaultDescription
tenant_iduuid
user_iduuid
sourcetext
created_attimestamp with time zonenow()
disabled_attimestamp with time zoneyes

Constraints:

  • PRIMARY KEY (tenant_id, user_id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (user_id) REFERENCES app_user(id) ON DELETE CASCADE
  • CHECK ((source = ANY (ARRAY['local'::text, 'oidc'::text, 'service'::text, 'billing'::text])))

Row-level security (forced): tenant_isolation.

billing_membership​

Service mode: the state of one membership the billing service pushed (docs/architecture.md §12). id is billing's membership id; version the newest applied, so older pushes change nothing. While status is active the user is a tenant_member (source billing) holding binding_id, the role binding the billing role maps to (admin: admin, member: viewer; NULL when an identical binding already existed). revoked removes the tenant_member row, and with it the user's bindings, tokens and team memberships in this tenant, and ends the user's sessions bound to it.

ColumnTypeNullDefaultDescription
idtext
tenant_iduuid
user_iduuid
roletext
statustext
versionbigint
binding_iduuidyes
updated_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (user_id) REFERENCES app_user(id)
  • CHECK ((role = ANY (ARRAY['admin'::text, 'member'::text])))
  • CHECK ((status = ANY (ARRAY['active'::text, 'revoked'::text])))

Row-level security (forced): tenant_isolation.

team​

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
slugtext
display_nametext
sourcetext
external_idtextyesOIDC group claim value
created_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, id)
  • UNIQUE (tenant_id, slug)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • CHECK ((slug ~ '^[a-z0-9][a-z0-9-]{0,62}$'::text))
  • CHECK ((source = ANY (ARRAY['local'::text, 'oidc'::text])))

Row-level security (forced): tenant_isolation.

team_member​

Members of oidc teams are synced at login from the groups claim (self-hosted installations); local teams are managed by hand. Service accounts are not team members; they are bound directly.

ColumnTypeNullDefaultDescription
tenant_iduuid
team_iduuid
user_iduuid
synced_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (team_id, user_id)
  • FOREIGN KEY (tenant_id, team_id) REFERENCES team(tenant_id, id) ON DELETE CASCADE
  • FOREIGN KEY (tenant_id, user_id) REFERENCES tenant_member(tenant_id, user_id) ON DELETE CASCADE
  • FOREIGN KEY (user_id) REFERENCES app_user(id) ON DELETE CASCADE

Row-level security (forced): tenant_isolation.

service_account​

The tenant-side details of a service account; id is its app_user id. Deleting the service account deletes its user, membership, tokens and role bindings. Once it has acted (authored a change set, reviewed, created a rollout, pin or operation), rows still reference its user and the deletion is refused (409 service_account.in_use): revoke its tokens and remove its role bindings instead.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
nametext
descriptiontextyes
owner_team_iduuidyes
created_byuuidyes
created_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, name)
  • FOREIGN KEY (created_by) REFERENCES app_user(id)
  • FOREIGN KEY (id) REFERENCES app_user(id) ON DELETE CASCADE
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, id) REFERENCES tenant_member(tenant_id, user_id) ON DELETE CASCADE
  • FOREIGN KEY (tenant_id, owner_team_id) REFERENCES team(tenant_id, id) ON DELETE SET NULL (owner_team_id)

Row-level security (forced): tenant_isolation.

role_binding​

Subject: a user (human or service account) or a team. Scope: the whole tenant, the objects owned by a team, or the hosts a selector matches (docs/authorization.md §3.3). admin is bound to the whole tenant only; tenant admins are its subjects. A team that scopes a binding cannot be deleted.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
user_iduuidyes
team_iduuidyes
rolerole_name
scope_kindtext
scope_team_iduuidyesscope_kind owner
scope_selectorjsonbyesscope_kind selector
created_byuuidyes
created_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE NULLS NOT DISTINCT (tenant_id, user_id, team_id, role, scope_kind, scope_team_id, scope_selector)
  • FOREIGN KEY (created_by) REFERENCES app_user(id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, scope_team_id) REFERENCES team(tenant_id, id)
  • FOREIGN KEY (tenant_id, team_id) REFERENCES team(tenant_id, id) ON DELETE CASCADE
  • FOREIGN KEY (tenant_id, user_id) REFERENCES tenant_member(tenant_id, user_id) ON DELETE CASCADE
  • CHECK ((((scope_kind = 'owner'::text) = (scope_team_id IS NOT NULL)) AND ((scope_kind = 'selector'::text) = (scope_selector IS NOT NULL))))
  • CHECK (((role <> 'admin'::role_name) OR (scope_kind = 'tenant'::text)))
  • CHECK ((num_nonnulls(user_id, team_id) = 1))
  • CHECK ((scope_kind = ANY (ARRAY['tenant'::text, 'owner'::text, 'selector'::text])))

Row-level security (forced): tenant_isolation.

api_token​

Tokens of a user or service account. Token = 'spk_' + 32 random bytes (base32). tenant_id NULL: platform token of a platform admin, valid for the platform API only. token_hash is unique across tenants: it is how a request finds its tenant's token.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuidyes
nametext
token_prefixtextfirst 8 chars, for display
token_hashbyteasha256(token)
user_iduuid
rolesrole_name[]yesoptional allowlist: the token acts with at most these of the owner's roles
expires_attimestamp with time zoneyes
last_used_attimestamp with time zoneyes
created_attimestamp with time zonenow()
revoked_attimestamp with time zoneyes

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (token_hash)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, user_id) REFERENCES tenant_member(tenant_id, user_id) ON DELETE CASCADE
  • FOREIGN KEY (user_id) REFERENCES app_user(id) ON DELETE CASCADE

Row-level security (forced): tenant_isolation.

web_session​

Web sessions (cookie sp_session). A session's data holds what it is bound to: the tenant chosen at login, the platform, or nothing yet (between login and the choice of a tenant). Choosing again replaces the session.

ColumnTypeNullDefaultDescription
idtext
databytea
expiry_datetimestamp with time zone

Constraints:

  • PRIMARY KEY (id)

Indexes:

  • web_session_expiry USING btree (expiry_date)

host​

Forgetting a host or agent instance deletes its rows; audit_event, secret_delivery and change_set_impact keep the ids without a foreign key.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
nametextFQDN or stable client name
kindhost_kind'server'::host_kind
owner_team_iduuidyes
labelsjsonb'{}'::jsonbeffective labels: host_label plus the built-in labels
first_seen_attimestamp with time zonenow()
updated_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, id)
  • UNIQUE (tenant_id, name)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, owner_team_id) REFERENCES team(tenant_id, id) ON DELETE SET NULL (owner_team_id)

Indexes:

  • host_labels_gin USING gin (labels jsonb_path_ops): The GIN index serves containment only (matchLabels); other selector predicates filter the tenant's rows (host_owner and the unique indexes start with tenant_id). It cannot lead with tenant_id without btree_gin.
  • host_owner USING btree (tenant_id, owner_team_id)

Row-level security (forced): tenant_isolation.

host_label​

Built-in labels (docs/spec-format.md §2) come from the host row; no source sets them, except os and arch, which only agent facts set.

ColumnTypeNullDefaultDescription
tenant_iduuid
host_iduuid
sourcelabel_source
keytext
valuetext
updated_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (host_id, source, key)
  • FOREIGN KEY (tenant_id, host_id) REFERENCES host(tenant_id, id) ON DELETE CASCADE
  • CHECK (((key ~ '^[a-z0-9]([a-z0-9_.\-/]{0,126}[a-z0-9])?$'::text) AND (key <> ALL (ARRAY['host.name'::text, 'host.kind'::text, 'owner'::text])) AND ((source = 'agent'::label_source) OR (key <> ALL (ARRAY['os'::text, 'arch'::text])))))
  • CHECK (((length(value) <= 256) AND (value !~ '[[:cntrl:]]'::text)))

Row-level security (forced): tenant_isolation.

network_zone​

Network zones: CIDR -> labels (source 'zone'), derived from agent source IPs. A source address matches the zone with the longest prefix among the zones of the agent's tenant (zones of different tenants may overlap). A host's zone labels come from the last source address of its oldest agent instance.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
cidrcidr
labelsjsonbe.g. {"zone": "dmz-1"}

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, cidr)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)

Indexes:

  • network_zone_cidr USING gist (cidr inet_ops)

Row-level security (forced): tenant_isolation.

spec_blob​

Canonical file content, content-addressed within the tenant.

ColumnTypeNullDefaultDescription
tenant_iduuid
hashbyteasha256(canonical bytes)
contentbyteacanonical TOML
created_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (tenant_id, hash)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)

Row-level security (forced): tenant_isolation.

spec_revision​

Each tenant has its own spec: revisions, blobs and change sets. History is native: log per file from spec_object validity ranges, diff between any two revision trees, revert and restore as ordinary change sets. Revisions and blobs are deleted only with their tenant.

Immutable revisions. The tree of revision N is the spec_object rows valid at N. History is linear and gapless: the parent of rN is rN-1 (r1 is the empty tree). A revert can recreate an earlier tree, so tree hashes repeat. The change set behind a revision is the one whose merged_revision_id it is.

ColumnTypeNullDefaultDescription
tenant_iduuid
idbigintr-number, from tenant_counter 'revision'
tree_hashbyteasha256 over the sorted (path, blob hash) pairs of the tree
messagetext
created_byuuidyes
created_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (tenant_id, id)
  • FOREIGN KEY (created_by) REFERENCES app_user(id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)

Row-level security (forced): tenant_isolation.

spec_object​

Object index with validity ranges, so "objects at revision N" is a range query without copying unchanged rows per revision. One object per file. A merge closes and opens ranges in the transaction that creates the revision, so the ranges of one object or path never overlap.

ColumnTypeNullDefaultDescription
tenant_iduuid
kindtext
nametext
pathtext
blob_hashbytea
owner_teamtextteam slug of the tenant
agent_kindagent_kindyesLayer, Bundle, Route (Journey: strataplane-agent)
agent_versionstextyessemver requirement, NULL = any
precedenceintegeryeslayers only
selectorjsonbyeslayers and journeys
valid_frombigint
valid_tobigintyesexclusive; NULL = still current

Constraints:

  • PRIMARY KEY (tenant_id, kind, name, valid_from)
  • FOREIGN KEY (tenant_id, blob_hash) REFERENCES spec_blob(tenant_id, hash)
  • FOREIGN KEY (tenant_id, valid_from) REFERENCES spec_revision(tenant_id, id)
  • FOREIGN KEY (tenant_id, valid_to) REFERENCES spec_revision(tenant_id, id)
  • CHECK ((((kind = 'Secret'::text) AND (name ~ '^[A-Za-z0-9_]+$'::text)) OR ((kind <> 'Secret'::text) AND (name ~ '^[a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?$'::text))))
  • CHECK (((kind = 'Layer'::text) OR (agent_versions IS NULL)))
  • CHECK (((kind = ANY (ARRAY['Layer'::text, 'Bundle'::text, 'Route'::text, 'Journey'::text])) = (agent_kind IS NOT NULL)))
  • CHECK ((kind = ANY (ARRAY['Layer'::text, 'Bundle'::text, 'Route'::text, 'Secret'::text, 'Journey'::text, 'RolloutPolicy'::text, 'ApprovalPolicy'::text, 'Settings'::text, 'Placement'::text])))
  • CHECK ((valid_to > valid_from))

Indexes:

  • spec_object_current UNIQUE USING btree (tenant_id, kind, name) WHERE (valid_to IS NULL)
  • spec_object_current_path UNIQUE USING btree (tenant_id, path) WHERE (valid_to IS NULL)
  • spec_object_path USING btree (tenant_id, path, valid_from DESC): per-file history

Row-level security (forced): tenant_isolation.

change_set​

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
numberbiginthuman handle CS-123, from tenant_counter 'change_set'
titletext
descriptiontext''::text
author_iduuiduser or service account
assistant_chat_iduuidyescreated by the assistant in this chat
base_revision_idbigint
statechange_set_state'draft'::change_set_state
digestbyteasha256 over (base, overlay); validation and approvals bind to it
validated_digestbyteayesdigest of the last successful validation
validationjsonbyesdiagnostics per file / artifact
impactjsonbyessummary: counts by kind, owner, stage, version
requirementsjsonbyescomputed approval requirements and their state
reverts_revision_idbigintyesa revert: the revision being undone
merged_revision_idbigintyes
created_attimestamp with time zonenow()
updated_attimestamp with time zonenow()
submitted_attimestamp with time zoneyes
merged_attimestamp with time zoneyes
closed_attimestamp with time zoneyes

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, id)
  • UNIQUE (tenant_id, merged_revision_id)
  • UNIQUE (tenant_id, number)
  • FOREIGN KEY (author_id) REFERENCES app_user(id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, base_revision_id) REFERENCES spec_revision(tenant_id, id)
  • FOREIGN KEY (tenant_id, merged_revision_id) REFERENCES spec_revision(tenant_id, id)
  • FOREIGN KEY (tenant_id, reverts_revision_id) REFERENCES spec_revision(tenant_id, id)
  • CHECK ((((state = 'merged'::change_set_state) = (merged_revision_id IS NOT NULL)) AND ((state = 'merged'::change_set_state) = (merged_at IS NOT NULL)) AND ((state = 'closed'::change_set_state) = (closed_at IS NOT NULL))))
  • CHECK (((state <> ALL (ARRAY['in_review'::change_set_state, 'merged'::change_set_state])) OR (NOT (validated_digest IS DISTINCT FROM digest))))

Row-level security (forced): tenant_isolation.

change_set_file​

The overlay: one row per changed path. blob_hash NULL = deletion.

ColumnTypeNullDefaultDescription
tenant_iduuid
change_set_iduuid
pathtext
blob_hashbyteayes
base_hashbyteayesblob at base revision (NULL = new file)
updated_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (change_set_id, path)
  • FOREIGN KEY (tenant_id, blob_hash) REFERENCES spec_blob(tenant_id, hash)
  • FOREIGN KEY (tenant_id, change_set_id) REFERENCES change_set(tenant_id, id) ON DELETE CASCADE

Row-level security (forced): tenant_isolation.

change_set_impact​

The impact set (docs/spec-format.md §6); before = after for agents affected only through a Secret or Placement. Large but bounded by fleet size; rows are deleted when the change set is merged or closed (the summary stays in change_set.impact).

ColumnTypeNullDefaultDescription
tenant_iduuid
change_set_iduuid
agent_instance_iduuidno FK: forgetting an agent keeps the row
before_artifactbyteayes
after_artifactbyteayesNULL = render failed, see validation

Constraints:

  • PRIMARY KEY (change_set_id, agent_instance_id)
  • FOREIGN KEY (tenant_id, after_artifact) REFERENCES artifact(tenant_id, hash)
  • FOREIGN KEY (tenant_id, before_artifact) REFERENCES artifact(tenant_id, hash)
  • FOREIGN KEY (tenant_id, change_set_id) REFERENCES change_set(tenant_id, id) ON DELETE CASCADE

Row-level security (forced): tenant_isolation.

change_set_review​

Reviewers are human users; how reviews count is described at change_set_state.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
change_set_iduuid
reviewer_iduuid
decisiontext
digestbyteacounts only while it equals change_set.digest
acting_teamstext[]'{}'::text[]teams this approval satisfies
commenttextyes
created_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • FOREIGN KEY (reviewer_id) REFERENCES app_user(id)
  • FOREIGN KEY (tenant_id, change_set_id) REFERENCES change_set(tenant_id, id) ON DELETE CASCADE
  • CHECK ((decision = ANY (ARRAY['approve'::text, 'reject'::text, 'comment'::text])))

Row-level security (forced): tenant_isolation.

enrollment_token​

token_hash is unique across tenants: the enrollment token names the tenant an agent enrolls into.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
nametext
agent_kindagent_kind
token_prefixtext
token_hashbytea
labelsjsonb'{}'::jsonbapplied to hosts enrolled with it (source 'enrollment')
host_kindhost_kind'server'::host_kind
modetext'enroll'::textenroll: exchanged for a per-agent credential. class (Telegraf only): the shared credential of every instance of the class (agents/telegraf.md, class mode). pair (strataplane-agent only): a pairing code of created_by, single use, enrolling a temporary exploration runner only that user may use (journeys.md §8.2); the instance's facts record paired_by.
requires_approvalbooleanfalse
max_usesintegeryes
usesinteger0
expires_attimestamp with time zoneyes
created_byuuidyes
created_attimestamp with time zonenow()
revoked_attimestamp with time zoneyes

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, id)
  • UNIQUE (token_hash)
  • FOREIGN KEY (created_by) REFERENCES app_user(id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • CHECK (((mode <> 'class'::text) OR (agent_kind = 'telegraf'::agent_kind)))
  • CHECK (((mode <> 'pair'::text) OR ((agent_kind = 'strataplane-agent'::agent_kind) AND (created_by IS NOT NULL))))
  • CHECK ((mode = ANY (ARRAY['enroll'::text, 'class'::text, 'pair'::text])))

Row-level security (forced): tenant_isolation.

trust_anchor​

CAs the tenant accepts for agent mTLS. Deleting an anchor first unbinds the certificates of the agents identified by it (identity_* NULL) in the same transaction, so they drop to assurance low.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
nametext
ca_pemtext
crl_urltextyessubject to the egress rules (docs/architecture.md §11)
host_mappingtext'san-dns'::text
created_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, id)
  • UNIQUE (tenant_id, name)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • CHECK ((host_mapping = ANY (ARRAY['san-dns'::text, 'cn'::text, 'san-uri-spiffe'::text])))

Row-level security (forced): tenant_isolation.

agent_instance​

The feedback loop: desired (rendered by the reconciler), served (fetched by Telegraf or Vector / POLICY_CHANGE delivered), applied (proven by the Telegraf or Vector heartbeat header, or Elastic ack plus checkin policy_revision_idx). A reported hash that is not an artifact of the tenant is ignored. An artifact proves content, not a revision: applied_revision_id is the revision whose render produced the applied artifact: the desired revision when the agent applied its desired artifact (or a later desired revision rendering the same bytes), kept while a newer desired artifact is not applied yet, and NULL when the agent applied an artifact that was not its desired one, whose revision the server does not know.

The bound client certificate. Bound means assurance high: every request that can carry a client cert must then present it.

With retention.silentAgents set (docs/spec-format.md §3.8), the retention job forgets an instance whose silence exceeds it, as a manual forget does (credentials revoked, rows deleted, audit event agent.forget by the system), at most 1000 per pass. Silence counts from the latest of last_seen_at (enrolled_at while it never reported) and the resumed_at of the tenant's controller_shard; the API shows that start plus silentAgents as the instance's forget_at. A host left without instances, manual labels and owner team is forgotten with it (host.forget).

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
host_iduuid
kindagent_kind
external_idtextTelegraf instance_id (class mode: hostname) / Fleet agent id / helper or Vector instance id
versiontextyesnormalized semver, e.g. 1.40.1
version_rawtextyesas reported
enrollment_token_iduuidyes
enrolled_attimestamp with time zonenow()
approved_attimestamp with time zoneyesNULL while first contact awaits approval
identity_subjecttextyescert subject/SAN
identity_fingerprintbyteayes
identity_anchor_iduuidyes
identity_not_aftertimestamp with time zoneyes
replace_token_hashbyteayesElastic Agent: SHA-256 of the replace_token sent at enrollment; a later enrollment with the same id re-binds the instance only with the same token (agents/elastic-agent.md §3).
assuranceassuranceyesgenerated: CASE WHEN (identity_fingerprint IS NULL) THEN 'low'::assurance ELSE 'high'::assurance END
healthagent_health'pending'::agent_health
health_messagetextyes
syncagent_sync'updating'::agent_sync
sync_reasontextyesout_of_sync reasons: reload_failed (served, not applied in time), watcher_stalled (Telegraf or Vector stopped polling), ambiguous_address (Telegraf class mode), restart_required (Vector rejected the reload because a global option changed). agents/telegraf.md §4, §6 and class mode; agents/vector.md §6.
flagstext[]'{}'::text[]unsupported_version: outside the supported range, receives no config; catalog_mismatch: no catalog bundle for the version, the nearest older one is used.
quarantined_attimestamp with time zoneyesno config updates, no secrets
quarantine_reasontextyes
desired_revision_idbigintyes
desired_artifactbyteayes
desired_sincetimestamp with time zoneyesdrives Telegraf Last-Modified; stable across replicas
served_artifactbyteayeslast artifact actually delivered (GET / action)
served_attimestamp with time zoneyes
applied_revision_idbigintyes
applied_artifactbyteayes
applied_attimestamp with time zoneyes
last_seen_attimestamp with time zoneyesnot indexed, so heartbeat updates stay HOT
last_source_ipinetyesPROXY protocol or X-Forwarded-For from agent_proxy_from
factsjsonb'{}'::jsonbreported metadata (ECS host, hostname, statistics, helper Chromium version)
created_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, id)
  • UNIQUE (tenant_id, kind, external_id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, applied_artifact) REFERENCES artifact(tenant_id, hash)
  • FOREIGN KEY (tenant_id, applied_revision_id) REFERENCES spec_revision(tenant_id, id)
  • FOREIGN KEY (tenant_id, desired_artifact) REFERENCES artifact(tenant_id, hash)
  • FOREIGN KEY (tenant_id, desired_revision_id) REFERENCES spec_revision(tenant_id, id)
  • FOREIGN KEY (tenant_id, enrollment_token_id) REFERENCES enrollment_token(tenant_id, id)
  • FOREIGN KEY (tenant_id, host_id) REFERENCES host(tenant_id, id) ON DELETE CASCADE
  • FOREIGN KEY (tenant_id, identity_anchor_id) REFERENCES trust_anchor(tenant_id, id)
  • FOREIGN KEY (tenant_id, served_artifact) REFERENCES artifact(tenant_id, hash)
  • CHECK (((quarantined_at IS NULL) = (quarantine_reason IS NULL)))
  • CHECK (((sync_reason IS NULL) OR (sync = 'out_of_sync'::agent_sync)))
  • CHECK ((flags <@ ARRAY['unsupported_version'::text, 'catalog_mismatch'::text]))
  • CHECK ((num_nulls(identity_subject, identity_fingerprint, identity_anchor_id, identity_not_after) = ANY (ARRAY[0, 4])))
  • CHECK ((sync_reason = ANY (ARRAY['reload_failed'::text, 'watcher_stalled'::text, 'ambiguous_address'::text, 'restart_required'::text])))

Indexes:

  • agent_instance_class USING btree (enrollment_token_id, last_source_ip) WHERE (enrollment_token_id IS NOT NULL): Telegraf class mode: the members of a class at a source address.
  • agent_instance_health USING btree (tenant_id, kind, health)
  • agent_instance_host USING btree (tenant_id, host_id)

Row-level security (forced): tenant_isolation.

class_default​

Telegraf class mode (agents/telegraf.md, class mode): the class config, rendered from the token's labels and the Telegraf version in User-Agent at the release pointer. Config requests get it unless their source address resolves to a member whose own desired artifact differs from it.

ColumnTypeNullDefaultDescription
tenant_iduuid
enrollment_token_iduuid
agent_versiontext
desired_revision_idbigintyes
desired_artifactbyteayes
desired_sincetimestamp with time zoneyes

Constraints:

  • PRIMARY KEY (enrollment_token_id, agent_version)
  • FOREIGN KEY (tenant_id, desired_artifact) REFERENCES artifact(tenant_id, hash)
  • FOREIGN KEY (tenant_id, desired_revision_id) REFERENCES spec_revision(tenant_id, id)
  • FOREIGN KEY (tenant_id, enrollment_token_id) REFERENCES enrollment_token(tenant_id, id) ON DELETE CASCADE

Row-level security (forced): tenant_isolation.

class_served​

Telegraf class mode: the artifact last served to a source address, for the watcher rule (agents/telegraf.md §6). Written only when it changes; rows without a GET for 30 days are pruned, and a missing row counts as different, which costs one extra reload.

ColumnTypeNullDefaultDescription
tenant_iduuid
enrollment_token_iduuid
addressinet
artifactbytea
served_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (enrollment_token_id, address)
  • FOREIGN KEY (tenant_id, artifact) REFERENCES artifact(tenant_id, hash)
  • FOREIGN KEY (tenant_id, enrollment_token_id) REFERENCES enrollment_token(tenant_id, id) ON DELETE CASCADE

Row-level security (forced): tenant_isolation.

agent_credential​

Per-agent credentials, by the instance's kind: Telegraf bearer tokens ('spt_'), Elastic access API keys ('spe_'), strataplane-agent bearer tokens ('sph_'), Vector bearer tokens ('spv_'). token_hash is unique across tenants. At most one is unrevoked per instance, so its last use is the instance's last_seen_at.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
agent_instance_iduuid
token_prefixtext
token_hashbytea
created_attimestamp with time zonenow()
revoked_attimestamp with time zoneyes

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (token_hash)
  • FOREIGN KEY (tenant_id, agent_instance_id) REFERENCES agent_instance(tenant_id, id) ON DELETE CASCADE

Indexes:

  • agent_credential_active UNIQUE USING btree (agent_instance_id) WHERE (revoked_at IS NULL)

Row-level security (forced): tenant_isolation.

agent_status_event​

transitions only, not every heartbeat

ColumnTypeNullDefaultDescription
idbigintnextval('agent_status_event_id_seq'::regclass)
tenant_iduuid
agent_instance_iduuid
tstimestamp with time zonenow()
fieldtext
from_valuetextyes
to_valuetext
detailjsonbyes

Constraints:

  • PRIMARY KEY (id)
  • FOREIGN KEY (tenant_id, agent_instance_id) REFERENCES agent_instance(tenant_id, id) ON DELETE CASCADE
  • CHECK ((field = ANY (ARRAY['health'::text, 'sync'::text, 'version'::text, 'applied'::text])))

Indexes:

  • agent_status_event_agent USING btree (agent_instance_id, ts DESC)

Row-level security (forced): tenant_isolation.

agent_log_entry​

from Telegraf heartbeat 'logs' and the internal_logs events of Vector heartbeats (source: Telegraf plugin::alias or Vector component id); the last 200 per agent, at most retention.agentLogs

ColumnTypeNullDefaultDescription
idbigintnextval('agent_log_entry_id_seq'::regclass)
tenant_iduuid
agent_instance_iduuid
tstimestamp with time zone
leveltext
sourcetexte.g. inputs.http::portal -> instance key
instance_keytextyes
messagetext
attributesjsonbyes

Constraints:

  • PRIMARY KEY (id)
  • FOREIGN KEY (tenant_id, agent_instance_id) REFERENCES agent_instance(tenant_id, id) ON DELETE CASCADE

Indexes:

  • agent_log_entry_agent USING btree (agent_instance_id, ts DESC)

Row-level security (forced): tenant_isolation.

agent_component​

Elastic Agent component/unit state from checkins (current state only).

ColumnTypeNullDefaultDescription
tenant_iduuid
agent_instance_iduuid
component_idtext
unit_idtext''::text'' = component-level row
unit_typetextyesinput | output
statetextSTARTING, HEALTHY, DEGRADED, FAILED, STOPPING, ...
messagetextyes
payloadjsonbyes
updated_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (agent_instance_id, component_id, unit_id)
  • FOREIGN KEY (tenant_id, agent_instance_id) REFERENCES agent_instance(tenant_id, id) ON DELETE CASCADE

Row-level security (forced): tenant_isolation.

agent_action​

Elastic Agent action queue. The instance's policy revision_idx is the policy_seq of its newest POLICY_CHANGE, which is never pruned.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
agent_instance_iduuid
typetext
datajsonb'{}'::jsonbnever contains secret values; policy is re-materialized on delivery
artifactbyteayesfor POLICY_CHANGE
policy_seqbigintyesrevision_idx carried by this policy
created_byuuidyes
created_attimestamp with time zonenow()
start_timetimestamp with time zoneyes
expires_attimestamp with time zoneyes
delivered_attimestamp with time zoneyes
delivery_countinteger0
acked_attimestamp with time zoneyes
ack_errortextyesPOLICY_CHANGE: the checkin's failure message, never an ack
superseded_byuuidyes

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, id)
  • FOREIGN KEY (created_by) REFERENCES app_user(id)
  • FOREIGN KEY (tenant_id, agent_instance_id) REFERENCES agent_instance(tenant_id, id) ON DELETE CASCADE
  • FOREIGN KEY (tenant_id, artifact) REFERENCES artifact(tenant_id, hash)
  • FOREIGN KEY (tenant_id, superseded_by) REFERENCES agent_action(tenant_id, id)
  • CHECK ((type = ANY (ARRAY['POLICY_CHANGE'::text, 'UNENROLL'::text, 'SETTINGS'::text])))

Indexes:

  • agent_action_pending USING btree (agent_instance_id, created_at) WHERE ((acked_at IS NULL) AND (superseded_by IS NULL))

Row-level security (forced): tenant_isolation.

artifact​

Rendered bytes, content-addressed. Renders that produce the same bytes from different inputs share one artifact, so an artifact carries no provenance: explain recomputes it from the render inputs (docs/architecture.md §4). Artifacts are per tenant even when two tenants render identical bytes. The format follows from the agent kind (TOML for Telegraf and Vector, JSON otherwise). Every artifact column points at a stored artifact, so garbage collection cannot delete one that is still in use.

ColumnTypeNullDefaultDescription
tenant_iduuid
hashbyteasha256(content)
agent_kindagent_kind
contentbyteazstd-compressed; secret references only, never values
secret_refstext[]'{}'::text[]'name@vN': the secret versions rendered against
sizeintegeruncompressed bytes
created_attimestamp with time zonenow()
last_used_attimestamp with time zonenow()GC: unreferenced and unused for retention.artifactsUnreferenced

Constraints:

  • PRIMARY KEY (tenant_id, hash)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)

Row-level security (forced): tenant_isolation.

artifact_validation​

L2/L3 results per distinct (artifact, agent version)

ColumnTypeNullDefaultDescription
tenant_iduuid
artifact_hashbytea
agent_versiontext
leveltext
okboolean
diagnosticsjsonb'[]'::jsonb
validatortexte.g. 'telegraf 1.40.1 config check'
created_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (tenant_id, artifact_hash, agent_version, level)
  • FOREIGN KEY (tenant_id, artifact_hash) REFERENCES artifact(tenant_id, hash) ON DELETE CASCADE
  • CHECK ((level = ANY (ARRAY['schema'::text, 'sandbox'::text])))

Row-level security (forced): tenant_isolation.

release​

Desired revision of an agent instance, first match wins: an active pin (agent, then selector, newest first); an assignment in an active rollout (its to_revision); the release pointer of its agent kind. The revision carries everything a render depends on besides inventory, secret versions and server.agent_url, including the render level (docs/architecture.md §4), so pins and rollouts fix renderer behaviour too. one row per tenant and agent kind

ColumnTypeNullDefaultDescription
tenant_iduuid
agent_kindagent_kind
revision_idbigint
rollout_iduuidyes
set_attimestamp with time zonenow()
set_byuuidyes

Constraints:

  • PRIMARY KEY (tenant_id, agent_kind)
  • FOREIGN KEY (set_by) REFERENCES app_user(id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, revision_id) REFERENCES spec_revision(tenant_id, id)
  • FOREIGN KEY (tenant_id, rollout_id) REFERENCES rollout(tenant_id, id)

Row-level security (forced): tenant_isolation.

rollout​

The change sets a rollout delivers are those merged into (from_revision, to_revision].

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
numberbigintRO-17, from tenant_counter 'rollout'
agent_kindagent_kind
from_revision_idbigint
to_revision_idbigint
policy_nametextyesRolloutPolicy used; NULL = single stage of all impacted agents
guardrailsjsonb'{}'::jsonbthe policy's guardrails at creation
staterollout_state'running'::rollout_state
created_byuuidyes
created_attimestamp with time zonenow()
finished_attimestamp with time zoneyes
halt_reasontextyeswhy the rollout halted (the breached guardrail); kept when the halted rollout is aborted, cleared when it is resumed

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, id)
  • UNIQUE (tenant_id, number)
  • FOREIGN KEY (created_by) REFERENCES app_user(id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, from_revision_id) REFERENCES spec_revision(tenant_id, id)
  • FOREIGN KEY (tenant_id, to_revision_id) REFERENCES spec_revision(tenant_id, id)
  • CHECK (((state = ANY (ARRAY['completed'::rollout_state, 'aborted'::rollout_state])) = (finished_at IS NOT NULL)))

Indexes:

  • rollout_one_active UNIQUE USING btree (tenant_id, agent_kind) WHERE (state = ANY (ARRAY['running'::rollout_state, 'paused'::rollout_state, 'halted'::rollout_state]))

Row-level security (forced): tenant_isolation.

rollout_stage​

ColumnTypeNullDefaultDescription
tenant_iduuid
rollout_iduuid
idxinteger
nametext
selectorjsonb
bake_timeinterval'00:00:00'::interval
staterollout_stage_state'pending'::rollout_stage_state
agents_totalintegeryesset when the stage starts
guardrailsjsonbyeslast evaluated values
started_attimestamp with time zoneyes
finished_attimestamp with time zoneyes

Constraints:

  • PRIMARY KEY (rollout_id, idx)
  • UNIQUE (tenant_id, rollout_id, idx)
  • FOREIGN KEY (tenant_id, rollout_id) REFERENCES rollout(tenant_id, id) ON DELETE CASCADE

Row-level security (forced): tenant_isolation.

rollout_assignment​

ColumnTypeNullDefaultDescription
tenant_iduuid
rollout_iduuid
agent_instance_iduuid
stage_idxinteger
assigned_attimestamp with time zonenow()
baseline_healthagent_healthyeshealth before the change, for regression guardrails

Constraints:

  • PRIMARY KEY (rollout_id, agent_instance_id)
  • FOREIGN KEY (tenant_id, agent_instance_id) REFERENCES agent_instance(tenant_id, id) ON DELETE CASCADE
  • FOREIGN KEY (tenant_id, rollout_id, stage_idx) REFERENCES rollout_stage(tenant_id, rollout_id, idx) ON DELETE CASCADE

Row-level security (forced): tenant_isolation.

pin​

A pin keeps its agents on its revision. Pins fix the revision, not the artifact: a new secret version still re-renders at the pinned revision and reaches the agent. Active from starts_at until expires_at. A pin or rollout to a revision that references a Secret absent at the latest revision is rejected, since that Secret is delivered to no one.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
selectorjsonbyeseither selector (with agent_kinds) or agent_instance_id
agent_kindsagent_kind[]yes
agent_instance_iduuidyes
revision_idbigint
reasontext
created_byuuidyes
created_attimestamp with time zonenow()
starts_attimestamp with time zonenow()
expires_attimestamp with time zoneyes

Constraints:

  • PRIMARY KEY (id)
  • FOREIGN KEY (created_by) REFERENCES app_user(id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, agent_instance_id) REFERENCES agent_instance(tenant_id, id) ON DELETE CASCADE
  • FOREIGN KEY (tenant_id, revision_id) REFERENCES spec_revision(tenant_id, id)
  • CHECK (((selector IS NULL) <> (agent_instance_id IS NULL)))
  • CHECK (((selector IS NULL) = (agent_kinds IS NULL)))
  • CHECK ((expires_at > starts_at))

Row-level security (forced): tenant_isolation.

secret_key​

Keys of the deployment's keyring (key-encryption keys): fingerprints only, never key material.

ColumnTypeNullDefaultDescription
idintegerkey id as in the keyring file
fingerprintbyteasha256 over a fixed label encrypted with the key
created_attimestamp with time zonenow()
exported_attimestamp with time zoneyesset by secrets export-key; readiness warns while NULL
retired_attimestamp with time zoneyesset: decrypts only, until rotation finishes

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (fingerprint)

tenant_key​

Per-tenant data keys, wrapped under a deployment key. They encrypt the tenant's sealed values and objects. Deleting a tenant's rows here makes its remaining ciphertext unreadable.

ColumnTypeNullDefaultDescription
tenant_iduuid
idinteger
kek_idinteger
noncebytea
wrapped_keybyteaAES-256-GCM(data key) under the deployment key, AAD = tenant id and key id
created_attimestamp with time zonenow()
retired_attimestamp with time zoneyesset: decrypts only, until rotation finishes

Constraints:

  • PRIMARY KEY (tenant_id, id)
  • FOREIGN KEY (kek_id) REFERENCES secret_key(id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)

Row-level security (forced): tenant_isolation.

sealed_value​

Every value encrypted under a tenant's data key: secret versions, LLM provider API keys, audit sink credentials. AES-256-GCM with AAD = tenant id and owner; decryption checks owner against the row that references the value, so a value cannot be moved to another owner. Written and deleted together with that row; rotating a data key re-encrypts this table.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
ownertext'secret//', 'llm_provider/', 'audit_sink/'
key_idinteger
noncebytea96-bit, random per value
ciphertextbyteanever returned by the API
created_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, id)
  • UNIQUE (tenant_id, owner)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, key_id) REFERENCES tenant_key(tenant_id, id)

Row-level security (forced): tenant_isolation.

secret_version​

Values of the spec Secret objects. The Secret object at the latest revision governs its values: its owner may write them (only while it exists there), and its grant and minAssurance decide delivery (docs/architecture.md §11). The current version of a name is the newest one not disabled. Versions belong to the name, not to a revision, so a Secret re-added under the same name gets its versions back.

ColumnTypeNullDefaultDescription
tenant_iduuid
secret_nametext
versioninteger
value_iduuid
commenttextyes
created_byuuidyes
created_attimestamp with time zonenow()
disabled_attimestamp with time zoneyes

Constraints:

  • PRIMARY KEY (tenant_id, secret_name, version)
  • FOREIGN KEY (created_by) REFERENCES app_user(id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, value_id) REFERENCES sealed_value(tenant_id, id)
  • CHECK ((secret_name ~ '^[A-Za-z0-9_]+$'::text))

Row-level security (forced): tenant_isolation.

secret_delivery​

audit of every delivery

ColumnTypeNullDefaultDescription
idbigintnextval('secret_delivery_id_seq'::regclass)
tenant_iduuid
secret_nametext
versioninteger
agent_instance_iduuid
channeltexttelegraf-secretstore: one row per key fetched; elastic-policy: per POLICY_CHANGE delivered; vector-config: when a config carrying the version is first served to the instance (served_artifact changes), not on every poll
assuranceassurance
identity_subjecttextyes
tstimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • CHECK ((channel = ANY (ARRAY['telegraf-secretstore'::text, 'elastic-policy'::text, 'vector-config'::text])))

Indexes:

  • secret_delivery_secret USING btree (tenant_id, secret_name, ts DESC)

Row-level security (forced): tenant_isolation.

catalog_bundle​

Installation-wide, imported by platform admins, read by every tenant. Import rejects bundles whose signature no trusted key verifies. Bundle files (wasm modules, validator binaries) are objects under catalog//.

ColumnTypeNullDefaultDescription
iduuid
agent_kindagent_kind
versiontextagent version this bundle describes
bundle_hashbytea
contentsjsonbmanifest: schemas, wasm modules (with their parsers and ABI version), validator binaries, with sizes and hashes
imported_byuuidyes
imported_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (agent_kind, version)
  • UNIQUE (bundle_hash)
  • FOREIGN KEY (imported_by) REFERENCES app_user(id)

plugin_schema​

ColumnTypeNullDefaultDescription
bundle_iduuid
sectiontextinputs, outputs, processors, aggregators, parsers, serializers, secretstores
plugintext
schemajsonbJSON Schema 2020-12
sampletextyessample.conf / reference config
docs_mdtextyes

Constraints:

  • PRIMARY KEY (bundle_id, section, plugin)
  • FOREIGN KEY (bundle_id) REFERENCES catalog_bundle(id) ON DELETE CASCADE

llm_provider​

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
nametext
kindtext
base_urltextsubject to the egress rules (docs/architecture.md §11)
api_key_iduuidyesAPI key: a server credential, not a spec Secret, so no agent config can reference it. Write-only, never sent to browsers.
modelsjsonb[{id, displayName, contextWindow, maxOutputTokens, protocol, thinking}]
enabledbooleantruedefault model and prompt addendum: Settings 'assistant'

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, name)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, api_key_id) REFERENCES sealed_value(tenant_id, id)
  • CHECK ((kind = ANY (ARRAY['openai-compatible'::text, 'anthropic'::text])))

Row-level security (forced): tenant_isolation.

chat​

One Pi Durable commit log per chat (docs/assistant.md §2). Opening a chat starts a new writer_epoch; a commit must carry the current epoch and seq = last_seq + 1, and a retry with last_seq and last_digest is accepted again without effect. Deleting a chat deletes its rows and keeps the row as a tombstone (deleted_at), so a view that still holds the chat cannot store it again.

ColumnTypeNullDefaultDescription
iduuidchosen by the client
tenant_iduuid
owner_iduuid
titletext''::text
writer_epochbigint0one writer at a time (tab takeover)
last_seqbigint0
last_digesttextyesopaque to the server, at most 512 bytes
stored_bytesbigint0sum of the rows' payload bytes; counts toward chat_bytes
created_attimestamp with time zonenow()
updated_attimestamp with time zonenow()
deleted_attimestamp with time zoneyes

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, id)
  • FOREIGN KEY (owner_id) REFERENCES app_user(id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • CHECK (((deleted_at IS NULL) OR (stored_bytes = 0)))

Indexes:

  • chat_recent USING btree (tenant_id, owner_id, updated_at DESC) WHERE (deleted_at IS NULL)

Row-level security (forced): tenant_isolation.

chat_row​

A commit's rows in order (pos). A row with replace deletes the chat's earlier rows with the same key before it is stored: task and submission records, and document bases. Document deltas carry their document's key without replace, so the next base removes them.

ColumnTypeNullDefaultDescription
tenant_iduuid
chat_iduuid
seqbigint
posinteger
keytextyese.g. task:, submission:, document:
replacebooleanfalse
payloadtextone Pi Durable storage write, JSON as sent

Constraints:

  • PRIMARY KEY (chat_id, seq, pos)
  • FOREIGN KEY (tenant_id, chat_id) REFERENCES chat(tenant_id, id) ON DELETE CASCADE
  • CHECK (((NOT replace) OR (key IS NOT NULL)))
  • CHECK ((length(key) <= 512))

Indexes:

  • chat_row_key USING btree (chat_id, key, seq) WHERE (key IS NOT NULL)

Row-level security (forced): tenant_isolation.

audit_event​

One hash chain per tenant. seq comes from tenant_counter 'audit', whose row lock is held until commit, so a tenant's events commit in seq order: the chain never forks and an export cursor on seq never skips an event. The event is the last write of its transaction, so the lock is held only for the commit.

ColumnTypeNullDefaultDescription
tenant_iduuid
seqbigint
tstimestamp with time zonenow()
actor_kindtextuser: human or service account; assistant: a request of the browser assistant naming its chat (X-Assistant-Chat, docs/assistant.md §1), actor_id the chat id
actor_idtextyes
on_behalf_ofuuidyesuser behind an assistant action, whose permissions it used
actiontexte.g. change_set.approve, secret.write, rollout.halt
object_kindtext
object_idtext
request_idtextyes
source_ipinetyes
detailsjsonb'{}'::jsonbuser events: details.authz = {package, bindings}, the policy package version and the role bindings that determined the request's permission decisions (docs/authorization.md §7)
prev_hashbyteayes
hashbyteasha256(prev_hash || canonical(event)); tamper evidence

Constraints:

  • PRIMARY KEY (tenant_id, seq)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • CHECK ((actor_kind = ANY (ARRAY['user'::text, 'agent'::text, 'system'::text, 'assistant'::text])))

Indexes:

  • audit_event_object USING btree (tenant_id, object_kind, object_id, ts DESC)

Row-level security (forced): tenant_isolation.

platform_audit_event​

Platform actions (tenants, catalog imports, platform admins): one chain for the installation. Appends hold a transaction advisory lock from taking the id until commit, for the same ordering guarantee.

ColumnTypeNullDefaultDescription
idbigintnextval('platform_audit_event_id_seq'::regclass)
tstimestamp with time zonenow()
actor_kindtext
actor_idtextyes
actiontexte.g. tenant.create, tenant.suspend, catalog.import
object_kindtext
object_idtext
tenant_iduuidyestenant concerned, if any; no FK, kept after deletion
request_idtextyes
source_ipinetyes
detailsjsonb'{}'::jsonb
prev_hashbyteayes
hashbytea

Constraints:

  • PRIMARY KEY (id)
  • CHECK ((actor_kind = ANY (ARRAY['user'::text, 'system'::text, 'idp'::text])))

audit_sink​

Export targets. config names the target and never holds credentials; a webhook's Authorization header value is the credential, write-only. A new sink starts after the tenant's latest event unless created with an explicit start; updating config or credential keeps the cursor.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
kindtext
configjsonbwebhook url; syslog address and tls
credential_iduuidyes
cursor_seqbigintlast exported audit_event.seq of the tenant
last_attempt_attimestamp with time zoneyes
last_errortextyesof the last attempt; NULL after a success
created_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, credential_id) REFERENCES sealed_value(tenant_id, id)
  • CHECK (((kind = 'webhook'::text) OR (credential_id IS NULL)))
  • CHECK ((kind = ANY (ARRAY['syslog'::text, 'webhook'::text])))

Row-level security (forced): tenant_isolation.

operation​

Long requests (validation, catalog import, tenant deletion) answer 202 with an operation. The work runs as an absurd task with the operation's id (schema "absurd", docs/stack.md), whose steps checkpoint in the same transaction as their writes here. This row is what the API shows; absurd's tables carry no tenant and are never read by request handlers. An operation stays queued, without a task, while its tenant has as many operations of its kind running as its limit allows (docs/architecture.md §12). tenant_id NULL: platform operation, visible to platform credentials only. One operation of a kind runs per object: requesting it again while it is queued or running returns that operation. The operation keeps its creator and the creator's token restriction and rechecks them before each privileged step (docs/authorization.md §6). Finished operations are deleted after 24 h.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuidyes
kindtexte.g. change_set.validate, catalog.import, tenant.delete
object_idtextwhat it works on (change set id, bundle hash, tenant id)
stateoperation_state'queued'::operation_state
progressrealyes
resultjsonbyes
errorjsonbyesproblem details
created_byuuidyes
created_attimestamp with time zonenow()
finished_attimestamp with time zoneyes

Constraints:

  • PRIMARY KEY (id)
  • FOREIGN KEY (created_by) REFERENCES app_user(id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • CHECK (((progress >= (0)::double precision) AND (progress <= (1)::double precision)))
  • CHECK (((state = 'failed'::operation_state) = (error IS NOT NULL)))
  • CHECK (((state = ANY (ARRAY['succeeded'::operation_state, 'failed'::operation_state])) = (finished_at IS NOT NULL)))

Indexes:

  • operation_active UNIQUE USING btree (kind, object_id) WHERE (state = ANY (ARRAY['queued'::operation_state, 'running'::operation_state]))
  • operation_finished USING btree (finished_at) WHERE (finished_at IS NOT NULL)
  • operation_tenant USING btree (tenant_id, kind, state)

Row-level security (forced): tenant_isolation.

idempotency_key​

Idempotency-Key of creating POSTs (docs/openapi.yaml), kept 24 h per principal and tenant (or platform). A request is authenticated first (credential validity, CSRF, an enabled user with an active membership and a role in the tenant, or a platform admin); then its key is claimed by inserting the row in a transaction that stays open while the request runs and stores the successful response (status, body, Content-Type, ETag, Location) before it commits. A concurrent repeat waits on the unique index until that transaction ends; a failed or streamed response is not stored, so its repeat runs again. A repeat returns the stored response; a repeat with a different request is rejected. Requests that return a credential take no key, so no stored response ever holds one.

ColumnTypeNullDefaultDescription
principal_iduuiduser or service account
keytext
tenant_iduuidyesNULL for platform requests
request_hashbyteasha256 over method, path and body
response_statusinteger
response_bodybytea
response_headersjsonb'{}'::jsonbcontent-type, etag, location
created_attimestamp with time zonenow()

Constraints:

  • UNIQUE NULLS NOT DISTINCT (principal_id, tenant_id, key)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • CHECK ((length(key) <= 255))

Indexes:

  • idempotency_key_created USING btree (created_at)

Row-level security (forced): tenant_isolation.

blob_delete​

Objects whose rows are gone, deleted by the blob sweeper. Object keys are derived from row ids, so a retried upload overwrites its own object.

ColumnTypeNullDefaultDescription
keytextobject key, or a prefix ending in '/'
enqueued_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (key)

journey_receipt​

Every accepted result, by its run id (UUIDv7 chosen by the runner), kept 24 h: the deduplication record. A result whose run id is already here is a duplicate whatever its content; the runner drops results older than 24 h, and the server rejects them (expired).

ColumnTypeNullDefaultDescription
tenant_iduuid
run_iduuid
agent_instance_iduuid
received_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (tenant_id, run_id)
  • FOREIGN KEY (tenant_id, agent_instance_id) REFERENCES agent_instance(tenant_id, id) ON DELETE CASCADE

Indexes:

  • journey_receipt_received USING btree (received_at)

Row-level security (forced): tenant_isolation.

journey_run​

Every run (journeys.md §6): failed and error runs up to failedRunsKept per journey, passed ones up to passedRunsKept; evidence only for failures. Scheduled runs also update journey_state, manual runs (journeys.md §3.1) never do. id is the run id (journey_receipt.run_id).

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
journey_nametext
agent_instance_iduuid
revision_idbigint
script_hashbyteasha256 of the script that ran
resultjourney_result
messagetextyesfailing assertion or error
stepsjsonb'[]'::jsonb[{name, offset_ms, duration_ms, result, error, line}]
outputtextyesprint/console output, at most 64 KiB
started_attimestamp with time zone
duration_msinteger
received_attimestamp with time zonenow()
triggered_byuuidyesmanual runs: who ran it; NULL for scheduled runs

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, id)
  • FOREIGN KEY (tenant_id, agent_instance_id) REFERENCES agent_instance(tenant_id, id) ON DELETE CASCADE
  • FOREIGN KEY (tenant_id, revision_id) REFERENCES spec_revision(tenant_id, id)
  • FOREIGN KEY (triggered_by) REFERENCES app_user(id)

Indexes:

  • journey_run_journey USING btree (tenant_id, journey_name, started_at DESC)
  • journey_run_result USING btree (tenant_id, journey_name, ((result = 'passed'::journey_result)), started_at DESC)

Row-level security (forced): tenant_isolation.

journey_state​

Latest result and counters per journey and runner; the source of the strataplane_journey_* series on the tenant's metrics endpoint (identical on every replica). Each receipt increments the counters once; the latest-run fields change only for a run that started after the stored one, so a delayed upload is counted without moving the state back.

ColumnTypeNullDefaultDescription
tenant_iduuid
journey_nametext
agent_instance_iduuid
revision_idbigint
script_hashbytea
resultjourney_result
messagetextyes
started_attimestamp with time zone
duration_msinteger
stepsjsonb'[]'::jsonb[{name, duration_ms, result}] of the latest run, for step metrics
failing_sincetimestamp with time zoneyesfirst non-passed run of the current streak
last_failed_run_iduuidyes
runs_passedbigint0
runs_failedbigint0
runs_errorbigint0
runs_skippedbigint0missed or overlapping slots, as reported by the runner
updated_attimestamp with time zonenow()

Constraints:

  • PRIMARY KEY (tenant_id, journey_name, agent_instance_id)
  • FOREIGN KEY (tenant_id, agent_instance_id) REFERENCES agent_instance(tenant_id, id) ON DELETE CASCADE
  • FOREIGN KEY (tenant_id, last_failed_run_id) REFERENCES journey_run(tenant_id, id) ON DELETE SET NULL (last_failed_run_id)
  • FOREIGN KEY (tenant_id, revision_id) REFERENCES spec_revision(tenant_id, id)

Row-level security (forced): tenant_isolation.

journey_evidence​

Screenshot and accessibility snapshot of the failing step of a run, stored as the object tenants//evidence//. The 2 MiB cap per run is enforced on upload. Pruned with the run.

ColumnTypeNullDefaultDescription
tenant_iduuid
run_iduuid
idxinteger
stepintegerindex into journey_run.steps
kindtext
media_typetext
sizeinteger
hashbytea

Constraints:

  • PRIMARY KEY (run_id, idx)
  • FOREIGN KEY (tenant_id, run_id) REFERENCES journey_run(tenant_id, id) ON DELETE CASCADE
  • CHECK ((kind = ANY (ARRAY['screenshot'::text, 'snapshot'::text])))
  • CHECK ((media_type = ANY (ARRAY['image/png'::text, 'image/jpeg'::text, 'application/json'::text])))

Row-level security (forced): tenant_isolation.

explore_session​

Requests of a user to a runner: a page on an exploration runner that the user, or the assistant acting as that user, drives ('session'), one trial run of a Journey file ('try', both docs/journeys.md §8.2), or one manual run of a deployed journey ('run', journeys.md §3.1). A session closes on request, after 10 minutes without a request, after 30 minutes, when the runner no longer holds it, or when its runner is deleted; a trial or manual run closes with its answer. Rows are deleted an hour after closing.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
agent_instance_iduuidthe exploration runner
kindtext
created_byuuid
start_urltext
allow_hoststext[]'{}'::text[]
created_attimestamp with time zonenow()
last_used_attimestamp with time zonenow()
closed_attimestamp with time zoneyes
close_reasontextyes

Constraints:

  • PRIMARY KEY (id)
  • UNIQUE (tenant_id, id)
  • FOREIGN KEY (created_by) REFERENCES app_user(id)
  • FOREIGN KEY (tenant_id) REFERENCES tenant(id)
  • FOREIGN KEY (tenant_id, agent_instance_id) REFERENCES agent_instance(tenant_id, id) ON DELETE CASCADE
  • CHECK (((closed_at IS NULL) = (close_reason IS NULL)))
  • CHECK ((close_reason = ANY (ARRAY['stopped'::text, 'idle'::text, 'expired'::text, 'runner_lost'::text, 'failed'::text, 'done'::text])))
  • CHECK ((kind = ANY (ARRAY['session'::text, 'try'::text, 'run'::text])))

Indexes:

  • explore_session_closed USING btree (closed_at) WHERE (closed_at IS NOT NULL)
  • explore_session_open USING btree (tenant_id, agent_instance_id) WHERE (closed_at IS NULL)

Row-level security (forced): tenant_isolation.

explore_request​

One request to the runner and its answer; a session has at most one unanswered request. The runner claims pending requests with its long-poll and answers each once; NOTIFY agent_actions with the runner id wakes the poll, with the request id the waiting API call. answer: outcome, output, page URL, title and snapshot, screenshots base64; at most 8 MiB.

ColumnTypeNullDefaultDescription
iduuid
tenant_iduuid
session_iduuid
agent_instance_iduuid
optext
bodyjsonb'{}'::jsonb
created_attimestamp with time zonenow()
claimed_attimestamp with time zoneyes
answered_attimestamp with time zoneyes
answerjsonbyes

Constraints:

  • PRIMARY KEY (id)
  • FOREIGN KEY (tenant_id, session_id) REFERENCES explore_session(tenant_id, id) ON DELETE CASCADE
  • CHECK (((answered_at IS NULL) = (answer IS NULL)))
  • CHECK ((op = ANY (ARRAY['open'::text, 'do'::text, 'observe'::text, 'close'::text, 'try'::text, 'run'::text])))

Indexes:

  • explore_request_pending USING btree (tenant_id, agent_instance_id, created_at) WHERE (claimed_at IS NULL)
  • explore_request_session USING btree (tenant_id, session_id)

Row-level security (forced): tenant_isolation.