Database Schema

July 18, 2026 · View on GitHub

This document is generated from the migrations. scripts/gen-db-schema.mjs applies every packages/backend/migrations/*.sql to a throwaway Postgres and introspects the result, so §3 below is the schema as it actually exists — not as someone remembered it. A hand-edit will be reverted by the next regeneration, and pnpm db:schema:check fails CI whenever a migration lands without the doc being regenerated.

Why it is generated. The previous hand-maintained doc documented ~20 tables while the migrations created 28. idempotency_keys was added with no doc entry and therefore never got the schema review this doc exists to enable; it shipped persisting credential-bearing response bodies in plaintext (remediated by 026_idempotency_no_store_bodies.sql). The documentation gap and the vulnerability were the same event.

Design intent (the why behind the DDL) lives in spec.md §28 (Persistence & State), §27 (Multi-Tenancy), §6 (Resource Model), §12.4 (Vault constraints), §13.5 (Memory versions), §17.8 (Scheduler exactly-once), and §23.6 (Webhook retry queue). The prose sections here (§1, §2, §4–§7) are hand-written commentary maintained in scripts/gen-db-schema.mjs; only §3 is machine-derived.

1. Principles

  1. Postgres holds metadata only. The JSONL session tree is the canonical conversation record and is NOT duplicated into Postgres (§28). Postgres holds a row per session for queryability (status, usage, config) — never the conversation itself.
  2. Every tenant-scoped table has tenant_id. Row-level filtering on tenantId is mandatory on every query (§27.1). The tenantScoped(query) helper in code makes the filter impossible to omit (compile-time: first arg is TenantCtx; runtime: asserts the SQL references tenant_id).
  3. Cross-tenant access is impossible by construction (§27.1).
  4. Forward-only migrations via node-pg-migrate (SQL files, §3.2). No heavyweight ORM; queries stay explicit so tenant filtering is auditable.
  5. Secrets never land in a column that was not designed for them. Ciphertext columns are bytea alongside key_id/nonce; response/​payload columns must never be used to persist a credential (see §4).

2. Conventions

ConventionChoiceRationale
tenant_idtext (prefixed, e.g. tnt_…)Consistent with the wire ID format (§6.6); IDs are opaque strings throughout.
Resource IDstext (prefixed per §6.6)agent_, env_, sess_, vault_, mem_, memver_, skill_, file_, job_, wh_; ULID payload, server-generated.
TimestampstimestamptzRFC 3339 UTC. Every resource has created_at; mutable ones have updated_at.
metadatajsonbMax 4 KiB serialized, keys [a-zA-Z0-9_.-]+, scalar values only (app-enforced, §8).
Secretsbytea (AES-256-GCM ciphertext + key_id + nonce)Never plaintext; never logged (§12.4, §28).
API keysargon2id hash onlyRaw key never stored (§8).
Soft deletestatus text column (active/archived/deleted)Agents/vaults use archive (terminal, audit trail); environments/files have hard delete too (§6.2, §21).
Enumstext + app-level validation, not Postgres ENUMAdding a value must not require a type migration.

3. Tables

Generated by introspecting a database with all packages/backend/migrations/*.sql applied. Column order is physical order; constraints and indexes are listed as Postgres reports them (pg_get_constraintdef / pg_get_indexdef), so what you read here is exactly what the database enforces.

31 tables (alphabetical):

agent_versions

Defined in: 003_agents.sql, 004_agent_versions.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
agent_idtextNOT NULL
versionintegerNOT NULL
configjsonbNOT NULL
created_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
agent_versions_agent_id_fkeyFOREIGN KEY (agent_id) REFERENCES agents(id)
agent_versions_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
agent_versions_pkeyPRIMARY KEY (agent_id, version)
IndexDefinition
idx_agent_versions_tenant_agentCREATE INDEX idx_agent_versions_tenant_agent ON public.agent_versions USING btree (tenant_id, agent_id)

agents

Defined in: 003_agents.sql, 004_agent_versions.sql, 006_sessions.sql, 014_jobs.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
idtextNOT NULL
nametextNOT NULL
current_versionintegerNOT NULL1
statustextNOT NULL'active'::text
metadatajsonbNOT NULL'{}'::jsonb
created_attimestamp with time zoneNOT NULLnow()
updated_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
agents_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
agents_pkeyPRIMARY KEY (id)
IndexDefinition
idx_agents_tenant_idCREATE INDEX idx_agents_tenant_id ON public.agents USING btree (tenant_id)
idx_agents_tenant_id_nameCREATE UNIQUE INDEX idx_agents_tenant_id_name ON public.agents USING btree (tenant_id, name)

api_keys

Defined in: 002_api_keys.sql, 025_onboarding.sql, 028_backfill_api_key_scopes.sql, 040_row_level_security.sql, 041_console_sessions.sql

ColumnTypeNullableDefault
idtextNOT NULL
tenant_idtextNOT NULL
key_hashtextNOT NULL
nametextNOT NULL
scopesjsonbNOT NULL'[]'::jsonb
last_used_attimestamp with time zonenullable
created_attimestamp with time zoneNOT NULLnow()
revoked_attimestamp with time zonenullable
ConstraintDefinition
api_keys_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
api_keys_pkeyPRIMARY KEY (id)
IndexDefinition
idx_api_keys_tenant_idCREATE INDEX idx_api_keys_tenant_id ON public.api_keys USING btree (tenant_id)

console_sessions

Defined in: 041_console_sessions.sql

ColumnTypeNullableDefault
token_hashtextNOT NULL
api_key_idtextNOT NULL
tenant_idtextNOT NULL
created_attimestamp with time zoneNOT NULLnow()
expires_attimestamp with time zoneNOT NULL
last_seen_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
console_sessions_api_key_id_fkeyFOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE
console_sessions_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
console_sessions_pkeyPRIMARY KEY (token_hash)
IndexDefinition
idx_console_sessions_api_key_idCREATE INDEX idx_console_sessions_api_key_id ON public.console_sessions USING btree (api_key_id)
idx_console_sessions_expires_atCREATE INDEX idx_console_sessions_expires_at ON public.console_sessions USING btree (expires_at)
idx_console_sessions_tenant_idCREATE INDEX idx_console_sessions_tenant_id ON public.console_sessions USING btree (tenant_id)

environments

Defined in: 005_environments.sql, 006_sessions.sql, 014_jobs.sql, 023_self_hosted_work_queue.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
idtextNOT NULL
nametextNOT NULL
typetextNOT NULL
imagetextNOT NULL
resourcesjsonbNOT NULL
networkingjsonbNOT NULL
packagesjsonbNOT NULL'[]'::jsonb
mountsjsonbNOT NULL'[]'::jsonb
max_durationintegernullable
idle_timeoutintegernullable
statustextNOT NULL'active'::text
metadatajsonbNOT NULL'{}'::jsonb
created_attimestamp with time zoneNOT NULLnow()
updated_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
environments_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
environments_pkeyPRIMARY KEY (id)
IndexDefinition
idx_environments_tenant_idCREATE INDEX idx_environments_tenant_id ON public.environments USING btree (tenant_id)
idx_environments_tenant_id_nameCREATE UNIQUE INDEX idx_environments_tenant_id_name ON public.environments USING btree (tenant_id, name)

files

Defined in: 006_sessions.sql, 011_files.sql, 038_quota_counter_file_storage.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
idtextNOT NULL
nametextNOT NULL
content_typetextnullable
size_bytesbigintNOT NULL
object_keytextNOT NULL
session_idtextnullable
metadatajsonbNOT NULL'{}'::jsonb
created_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
files_session_id_fkeyFOREIGN KEY (session_id) REFERENCES sessions(id)
files_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
files_pkeyPRIMARY KEY (id)
IndexDefinition
idx_files_tenant_idCREATE INDEX idx_files_tenant_id ON public.files USING btree (tenant_id)
idx_files_tenant_sessionCREATE INDEX idx_files_tenant_session ON public.files USING btree (tenant_id, session_id)

idempotency_keys

Defined in: 021_idempotency_keys.sql, 026_idempotency_no_store_bodies.sql, 039_idempotency_claim_lease.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
key_hashtextNOT NULL
request_body_hashtextNOT NULL
response_statusintegerNOT NULL
response_bodytextnullable
created_attimestamp with time zoneNOT NULLnow()
expires_attimestamp with time zoneNOT NULL
claimed_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
idempotency_keys_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
idempotency_keys_tenant_id_key_hash_keyUNIQUE (tenant_id, key_hash)
IndexDefinition
idx_idempotency_keys_expires_atCREATE INDEX idx_idempotency_keys_expires_at ON public.idempotency_keys USING btree (expires_at)

job_runs

Defined in: 015_job_runs.sql, 034_job_runs_claim.sql, 039_idempotency_claim_lease.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
idtextNOT NULL
job_idtextNOT NULL
scheduled_attimestamp with time zoneNOT NULL
triggered_attimestamp with time zonenullable
session_idtextnullable
manualbooleanNOT NULLfalse
errorjsonbnullable
created_attimestamp with time zoneNOT NULLnow()
claimed_attimestamp with time zonenullable
ConstraintDefinition
job_runs_job_id_fkeyFOREIGN KEY (job_id) REFERENCES jobs(id)
job_runs_session_id_fkeyFOREIGN KEY (session_id) REFERENCES sessions(id)
job_runs_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
job_runs_pkeyPRIMARY KEY (id)
IndexDefinition
idx_job_runs_job_scheduledCREATE UNIQUE INDEX idx_job_runs_job_scheduled ON public.job_runs USING btree (job_id, scheduled_at)
idx_job_runs_recoveryCREATE INDEX idx_job_runs_recovery ON public.job_runs USING btree (triggered_at, claimed_at, scheduled_at)
idx_job_runs_tenant_createdCREATE INDEX idx_job_runs_tenant_created ON public.job_runs USING btree (tenant_id, created_at)

jobs

Defined in: 014_jobs.sql, 015_job_runs.sql, 031_tenant_quota_counters.sql, 032_jobs_status_index.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
idtextNOT NULL
nametextNOT NULL
agent_idtextNOT NULL
agent_versionintegerNOT NULL
environment_idtextNOT NULL
initial_eventsjsonbNOT NULL
session_configjsonbNOT NULL'{}'::jsonb
schedule_crontextNOT NULL
schedule_tztextNOT NULL
one_shotbooleanNOT NULLfalse
statustextNOT NULL'active'::text
paused_reasonjsonbnullable
created_attimestamp with time zoneNOT NULLnow()
updated_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
jobs_agent_id_fkeyFOREIGN KEY (agent_id) REFERENCES agents(id)
jobs_environment_id_fkeyFOREIGN KEY (environment_id) REFERENCES environments(id)
jobs_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
jobs_pkeyPRIMARY KEY (id)
IndexDefinition
idx_jobs_statusCREATE INDEX idx_jobs_status ON public.jobs USING btree (status) WHERE (status = 'active'::text)
idx_jobs_tenant_idCREATE INDEX idx_jobs_tenant_id ON public.jobs USING btree (tenant_id)
idx_jobs_tenant_id_nameCREATE UNIQUE INDEX idx_jobs_tenant_id_name ON public.jobs USING btree (tenant_id, name)

ledger_entries

Defined in: 042_billing_ledger.sql

ColumnTypeNullableDefault
idtextNOT NULL
tenant_idtextNOT NULL
kindtextNOT NULL
amount_microsbigintNOT NULL
balance_after_microsbigintNOT NULL
idempotency_keytextNOT NULL
sourcetextnullable
metadatajsonbnullable
created_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
ledger_amount_signCHECK ((((kind = ANY (ARRAY['grant'::text, 'topup'::text])) AND (amount_micros > 0)) OR ((kind = 'debit'::text) AND (amount_micros < 0)) OR (kind = 'adjustment'::text)))
ledger_entries_amount_micros_checkCHECK ((amount_micros <> 0))
ledger_entries_kind_checkCHECK ((kind = ANY (ARRAY['grant'::text, 'topup'::text, 'debit'::text, 'adjustment'::text])))
ledger_entries_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
ledger_entries_pkeyPRIMARY KEY (id)
ledger_entries_tenant_id_idempotency_key_keyUNIQUE (tenant_id, idempotency_key)
IndexDefinition
idx_ledger_entries_tenant_createdCREATE INDEX idx_ledger_entries_tenant_created ON public.ledger_entries USING btree (tenant_id, created_at DESC, id DESC)

memory_stores

Defined in: 009_memory_stores.sql, 010_memory_versions.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
idtextNOT NULL
display_titletextNOT NULL
instructionstextnullable
accesstextNOT NULL'read_write'::text
statustextNOT NULL'active'::text
object_key_prefixtextNOT NULL
created_attimestamp with time zoneNOT NULLnow()
updated_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
memory_stores_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
memory_stores_pkeyPRIMARY KEY (id)
IndexDefinition
idx_memory_stores_tenant_idCREATE INDEX idx_memory_stores_tenant_id ON public.memory_stores USING btree (tenant_id)
idx_memory_stores_tenant_titleCREATE UNIQUE INDEX idx_memory_stores_tenant_title ON public.memory_stores USING btree (tenant_id, display_title)

memory_versions

Defined in: 010_memory_versions.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
store_idtextNOT NULL
idtextNOT NULL
memory_pathtextnullable
content_sha256textNOT NULL
content_object_keytextnullable
redactedbooleanNOT NULLfalse
created_attimestamp with time zoneNOT NULLnow()
expires_attimestamp with time zonenullable
ConstraintDefinition
memory_versions_store_id_fkeyFOREIGN KEY (store_id) REFERENCES memory_stores(id)
memory_versions_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
memory_versions_pkeyPRIMARY KEY (id)
IndexDefinition
idx_memory_versions_store_createdCREATE INDEX idx_memory_versions_store_created ON public.memory_versions USING btree (store_id, created_at)
idx_memory_versions_tenant_idCREATE INDEX idx_memory_versions_tenant_id ON public.memory_versions USING btree (tenant_id)

onboarding_signups

Defined in: 025_onboarding.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
admin_emailtextNOT NULL
tenant_idtextNOT NULL
created_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
onboarding_signups_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
onboarding_signups_pkeyPRIMARY KEY (admin_email)
IndexDefinition
idx_onboarding_signups_tenant_idCREATE INDEX idx_onboarding_signups_tenant_id ON public.onboarding_signups USING btree (tenant_id)

rate_limit_buckets

Defined in: 027_rate_limit_buckets.sql

ColumnTypeNullableDefault
keytextNOT NULL
tokensdouble precisionNOT NULL
capacitydouble precisionNOT NULL
refill_per_msdouble precisionNOT NULL
allowedbooleanNOT NULLtrue
last_refill_msbigintNOT NULL
updated_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
rate_limit_buckets_pkeyPRIMARY KEY (key)
IndexDefinition
idx_rate_limit_buckets_last_refillCREATE INDEX idx_rate_limit_buckets_last_refill ON public.rate_limit_buckets USING btree (last_refill_ms)

sandbox_host_placements

Defined in: 024_sandbox_hosts.sql, 036_audit_remediation.sql

ColumnTypeNullableDefault
sandbox_nametextNOT NULL
host_idtextNOT NULL
created_attimestamp with time zoneNOT NULLnow()
cpusintegerNOT NULL1
memory_mibintegerNOT NULL512
ConstraintDefinition
sandbox_host_placements_host_id_fkeyFOREIGN KEY (host_id) REFERENCES sandbox_hosts(id)
sandbox_host_placements_pkeyPRIMARY KEY (sandbox_name)
IndexDefinition
idx_sandbox_host_placements_hostCREATE INDEX idx_sandbox_host_placements_host ON public.sandbox_host_placements USING btree (host_id)

sandbox_hosts

Defined in: 024_sandbox_hosts.sql

ColumnTypeNullableDefault
idtextNOT NULL
endpointtextNOT NULL
cpusintegerNOT NULL
memory_mibintegerNOT NULL
statustextNOT NULL'healthy'::text
labelsjsonbNOT NULL'{}'::jsonb
last_heartbeattimestamp with time zonenullable
created_attimestamp with time zoneNOT NULLnow()
updated_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
sandbox_hosts_pkeyPRIMARY KEY (id)
IndexDefinition
idx_sandbox_hosts_statusCREATE INDEX idx_sandbox_hosts_status ON public.sandbox_hosts USING btree (status)

self_hosted_work_queue

Defined in: 023_self_hosted_work_queue.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
idtextNOT NULL
session_idtextNOT NULL
environment_idtextNOT NULL
statustextNOT NULL'queued'::text
work_specjsonbNOT NULL
resultsjsonbNOT NULL'[]'::jsonb
claimed_bytextnullable
claimed_attimestamp with time zonenullable
queued_attimestamp with time zoneNOT NULLnow()
completed_attimestamp with time zonenullable
stop_requestedtextnullable
stop_requested_attimestamp with time zonenullable
created_attimestamp with time zoneNOT NULLnow()
updated_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
self_hosted_work_queue_environment_id_fkeyFOREIGN KEY (environment_id) REFERENCES environments(id)
self_hosted_work_queue_session_id_fkeyFOREIGN KEY (session_id) REFERENCES sessions(id)
self_hosted_work_queue_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
self_hosted_work_queue_pkeyPRIMARY KEY (id)
IndexDefinition
idx_self_hosted_work_queue_env_statusCREATE INDEX idx_self_hosted_work_queue_env_status ON public.self_hosted_work_queue USING btree (environment_id, status, queued_at)
idx_self_hosted_work_queue_sessionCREATE UNIQUE INDEX idx_self_hosted_work_queue_session ON public.self_hosted_work_queue USING btree (session_id)
idx_self_hosted_work_queue_tenantCREATE INDEX idx_self_hosted_work_queue_tenant ON public.self_hosted_work_queue USING btree (tenant_id)

session_events

Defined in: 030_session_events.sql, 035_session_events_unique_event_id.sql, 037_session_events_retention.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
session_idtextNOT NULL
positionbigintNOT NULL
typetextNOT NULL
event_idtextNOT NULL
created_attimestamp with time zoneNOT NULLnow()
payloadjsonbNOT NULL
ConstraintDefinition
session_events_session_id_fkeyFOREIGN KEY (session_id) REFERENCES sessions(id)
session_events_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
session_events_pkeyPRIMARY KEY (session_id, "position")
IndexDefinition
idx_session_events_created_atCREATE INDEX idx_session_events_created_at ON public.session_events USING btree (created_at)
idx_session_events_session_event_idCREATE UNIQUE INDEX idx_session_events_session_event_id ON public.session_events USING btree (session_id, event_id)
idx_session_events_tenant_idCREATE INDEX idx_session_events_tenant_id ON public.session_events USING btree (tenant_id)

session_outcomes

Defined in: 018_session_outcomes.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
session_idtextNOT NULL
idtextNOT NULL
descriptiontextNOT NULL
rubricjsonbNOT NULL
max_iterationsintegerNOT NULL3
statustextNOT NULL
resulttextnullable
iterationintegerNOT NULL0
created_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
session_outcomes_session_id_fkeyFOREIGN KEY (session_id) REFERENCES sessions(id)
session_outcomes_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
session_outcomes_pkeyPRIMARY KEY (id)
IndexDefinition
idx_session_outcomes_tenant_sessionCREATE INDEX idx_session_outcomes_tenant_session ON public.session_outcomes USING btree (tenant_id, session_id)

session_threads

Defined in: 019_session_threads.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
session_idtextNOT NULL
thread_idtextNOT NULL
agent_nametextNOT NULL
statustextNOT NULL
created_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
session_threads_session_id_fkeyFOREIGN KEY (session_id) REFERENCES sessions(id)
session_threads_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
session_threads_pkeyPRIMARY KEY (session_id, thread_id)
IndexDefinition
idx_session_threads_tenant_sessionCREATE INDEX idx_session_threads_tenant_session ON public.session_threads USING btree (tenant_id, session_id)

sessions

Defined in: 003_agents.sql, 006_sessions.sql, 011_files.sql, 015_job_runs.sql, 018_session_outcomes.sql, 019_session_threads.sql, 020_usage_records.sql, 023_self_hosted_work_queue.sql, 029_sessions_synced_etag.sql, 030_session_events.sql, 031_tenant_quota_counters.sql, 034_job_runs_claim.sql, 036_audit_remediation.sql, 040_row_level_security.sql, 041_console_sessions.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
idtextNOT NULL
agent_idtextNOT NULL
agent_versionintegerNOT NULL
environment_idtextNOT NULL
titletextnullable
statustextNOT NULL'idle'::text
stop_reasontextnullable
budgetjsonbnullable
usagejsonbNOT NULL'{}'::jsonb
vault_idsjsonbNOT NULL'[]'::jsonb
resourcesjsonbNOT NULL'[]'::jsonb
agent_overridesjsonbnullable
metadatajsonbNOT NULL'{}'::jsonb
created_attimestamp with time zoneNOT NULLnow()
updated_attimestamp with time zoneNOT NULLnow()
last_activity_attimestamp with time zoneNOT NULLnow()
jsonl_object_keytextNOT NULL
forked_from_session_idtextnullable
sandbox_handletextnullable
synced_etagtextnullable
owner_instance_idtextnullable
ConstraintDefinition
sessions_agent_id_fkeyFOREIGN KEY (agent_id) REFERENCES agents(id)
sessions_environment_id_fkeyFOREIGN KEY (environment_id) REFERENCES environments(id)
sessions_forked_from_session_id_fkeyFOREIGN KEY (forked_from_session_id) REFERENCES sessions(id)
sessions_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
sessions_pkeyPRIMARY KEY (id)
IndexDefinition
idx_sessions_tenant_agentCREATE INDEX idx_sessions_tenant_agent ON public.sessions USING btree (tenant_id, agent_id)
idx_sessions_tenant_created_idCREATE INDEX idx_sessions_tenant_created_id ON public.sessions USING btree (tenant_id, created_at DESC, id DESC) WHERE (status <> 'archived'::text)
idx_sessions_tenant_environmentCREATE INDEX idx_sessions_tenant_environment ON public.sessions USING btree (tenant_id, environment_id)
idx_sessions_tenant_statusCREATE INDEX idx_sessions_tenant_status ON public.sessions USING btree (tenant_id, status)

skill_versions

Defined in: 013_skill_versions.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
skill_idtextNOT NULL
versionintegerNOT NULL
object_keytextNOT NULL
created_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
skill_versions_skill_id_fkeyFOREIGN KEY (skill_id) REFERENCES skills(id)
skill_versions_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
skill_versions_pkeyPRIMARY KEY (skill_id, version)
IndexDefinition
idx_skill_versions_tenant_skillCREATE INDEX idx_skill_versions_tenant_skill ON public.skill_versions USING btree (tenant_id, skill_id)

skills

Defined in: 004_agent_versions.sql, 012_skills.sql, 013_skill_versions.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
idtextNOT NULL
display_titletextNOT NULL
typetextNOT NULL
created_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
skills_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
skills_pkeyPRIMARY KEY (id)
IndexDefinition
idx_skills_tenant_idCREATE INDEX idx_skills_tenant_id ON public.skills USING btree (tenant_id)
idx_skills_tenant_title_customCREATE UNIQUE INDEX idx_skills_tenant_title_custom ON public.skills USING btree (tenant_id, display_title) WHERE (type = 'custom'::text)

tenant_billing

Defined in: 042_billing_ledger.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
lifecycletextNOT NULL'trial'::text
balance_microsbigintNOT NULL0
verification_token_hashtextnullable
verification_expires_attimestamp with time zonenullable
verified_attimestamp with time zonenullable
pending_grant_microsbigintNOT NULL0
created_attimestamp with time zoneNOT NULLnow()
updated_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
tenant_billing_lifecycle_checkCHECK ((lifecycle = ANY (ARRAY['trial'::text, 'active'::text, 'suspended'::text])))
tenant_billing_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
tenant_billing_pkeyPRIMARY KEY (tenant_id)

tenant_quota_counters

Defined in: 031_tenant_quota_counters.sql, 038_quota_counter_file_storage.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
resourcetextNOT NULL
countbigintNOT NULL0
ConstraintDefinition
tenant_quota_counters_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
tenant_quota_counters_pkeyPRIMARY KEY (tenant_id, resource)

tenants

Defined in: 001_tenants.sql, 002_api_keys.sql, 003_agents.sql, 004_agent_versions.sql, 005_environments.sql, 006_sessions.sql, 007_vaults.sql, 008_vault_credentials.sql, 009_memory_stores.sql, 010_memory_versions.sql, 011_files.sql, 012_skills.sql, 013_skill_versions.sql, 014_jobs.sql, 015_job_runs.sql, 016_webhooks.sql, 017_webhook_deliveries.sql, 018_session_outcomes.sql, 019_session_threads.sql, 020_usage_records.sql, 021_idempotency_keys.sql, 023_self_hosted_work_queue.sql, 024_sandbox_hosts.sql, 025_onboarding.sql, 030_session_events.sql, 031_tenant_quota_counters.sql, 032_jobs_status_index.sql, 037_session_events_retention.sql, 041_console_sessions.sql, 042_billing_ledger.sql

ColumnTypeNullableDefault
idtextNOT NULL
nametextNOT NULL
quota_plantextnullable
created_attimestamp with time zoneNOT NULLnow()
updated_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
tenants_pkeyPRIMARY KEY (id)

usage_records

Defined in: 020_usage_records.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
session_idtextNOT NULL
modeltextNOT NULL
input_tokensbigintNOT NULL0
output_tokensbigintNOT NULL0
cache_creation_input_tokensbigintNOT NULL0
cache_read_input_tokensbigintNOT NULL0
usd_costnumeric(12,6)NOT NULL0
recorded_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
usage_records_session_id_fkeyFOREIGN KEY (session_id) REFERENCES sessions(id)
usage_records_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
IndexDefinition
idx_usage_records_tenant_recordedCREATE INDEX idx_usage_records_tenant_recorded ON public.usage_records USING btree (tenant_id, recorded_at)
idx_usage_records_tenant_session_recordedCREATE INDEX idx_usage_records_tenant_session_recorded ON public.usage_records USING btree (tenant_id, session_id, recorded_at)

vault_credentials

Defined in: 008_vault_credentials.sql, 022_vault_credentials_id.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
vault_idtextNOT NULL
keytextNOT NULL
categorytextNOT NULL
secret_encbyteanullable
key_idtextnullable
noncebyteanullable
metadatajsonbNOT NULL'{}'::jsonb
statustextNOT NULL'active'::text
created_attimestamp with time zoneNOT NULLnow()
updated_attimestamp with time zoneNOT NULLnow()
idtextNOT NULL
ConstraintDefinition
vault_credentials_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
vault_credentials_vault_id_fkeyFOREIGN KEY (vault_id) REFERENCES vaults(id)
vault_credentials_pkeyPRIMARY KEY (id)
IndexDefinition
idx_vault_credentials_tenant_idCREATE INDEX idx_vault_credentials_tenant_id ON public.vault_credentials USING btree (tenant_id)
idx_vault_credentials_vault_keyCREATE UNIQUE INDEX idx_vault_credentials_vault_key ON public.vault_credentials USING btree (vault_id, key)

vaults

Defined in: 007_vaults.sql, 008_vault_credentials.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
idtextNOT NULL
nametextNOT NULL
statustextNOT NULL'active'::text
created_attimestamp with time zoneNOT NULLnow()
updated_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
vaults_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
vaults_pkeyPRIMARY KEY (id)
IndexDefinition
idx_vaults_tenant_idCREATE INDEX idx_vaults_tenant_id ON public.vaults USING btree (tenant_id)
idx_vaults_tenant_id_nameCREATE UNIQUE INDEX idx_vaults_tenant_id_name ON public.vaults USING btree (tenant_id, name)

webhook_deliveries

Defined in: 017_webhook_deliveries.sql, 033_webhook_delivery_safety.sql, 034_job_runs_claim.sql, 039_idempotency_claim_lease.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
idtextNOT NULL
webhook_idtextNOT NULL
event_idtextNOT NULL
event_typetextNOT NULL
payloadjsonbNOT NULL
attemptintegerNOT NULL1
statustextNOT NULL'pending'::text
next_attempt_attimestamp with time zonenullable
last_response_codeintegernullable
created_attimestamp with time zoneNOT NULLnow()
claimed_attimestamp with time zonenullable
ConstraintDefinition
webhook_deliveries_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
webhook_deliveries_webhook_id_fkeyFOREIGN KEY (webhook_id) REFERENCES webhooks(id)
webhook_deliveries_pkeyPRIMARY KEY (id)
IndexDefinition
idx_webhook_deliveries_pending_next_attemptCREATE INDEX idx_webhook_deliveries_pending_next_attempt ON public.webhook_deliveries USING btree (next_attempt_at) WHERE (status = 'pending'::text)
idx_webhook_deliveries_tenant_idCREATE INDEX idx_webhook_deliveries_tenant_id ON public.webhook_deliveries USING btree (tenant_id)
idx_webhook_deliveries_webhook_eventCREATE UNIQUE INDEX idx_webhook_deliveries_webhook_event ON public.webhook_deliveries USING btree (webhook_id, event_id)

webhooks

Defined in: 016_webhooks.sql, 017_webhook_deliveries.sql, 033_webhook_delivery_safety.sql, 040_row_level_security.sql

ColumnTypeNullableDefault
tenant_idtextNOT NULL
idtextNOT NULL
urltextNOT NULL
signing_secret_hashtextNOT NULL
event_typesjsonbNOT NULL'[]'::jsonb
statustextNOT NULL'active'::text
disabled_reasontextnullable
created_attimestamp with time zoneNOT NULLnow()
updated_attimestamp with time zoneNOT NULLnow()
ConstraintDefinition
webhooks_tenant_id_fkeyFOREIGN KEY (tenant_id) REFERENCES tenants(id)
webhooks_pkeyPRIMARY KEY (id)
IndexDefinition
idx_webhooks_event_typesCREATE INDEX idx_webhooks_event_types ON public.webhooks USING gin (event_types jsonb_path_ops)
idx_webhooks_tenant_idCREATE INDEX idx_webhooks_tenant_id ON public.webhooks USING btree (tenant_id)

4. Encrypted-Column Strategy

vault_credentials.secret_enc is stored as AES-256-GCM ciphertext, alongside key_id (which KMS/keyfile key was used) and nonce (the GCM nonce).

  • Key source: KMS-managed key for SaaS; a key file for self-hosted (§28).
  • The key is required for restore and must be backed up separately from the database (§28). Losing the key = losing all vault credentials.
  • Encryption/decryption happens in the application layer (the SecretStore port, §25.3); Postgres never sees plaintext.
  • On credential archive, secret_enc/key_id/nonce are set to null (secret purged, key freed, audit row retained — §12.7).
  • API-key hashing uses argon2id (§8) — distinct from secret encryption; the raw key is never stored, only the hash.

Columns that must never hold a credential

Table.columnRule
idempotency_keys.response_bodyNullable by design. Credential-issuing routes (API-key create, webhook create/rotate) record the idempotency key with a NULL body; a replay yields 409 idempotency_conflict rather than re-serving a pmb_live_/whsec_ secret (026_idempotency_no_store_bodies.sql, §25.1, §8).
webhook_deliveries.payloadThin payload only — type + id + createdAt (§23.2). Never the resource body.
api_keys.key_hashargon2id hash of the key, never the key.
webhooks.signing_secret_hashHash of the whsec_ secret, never the secret.

5. What is NOT in Postgres

Explicit list (§28):

StateStore
Session JSONL tree (conversation)Object store (<key from sessions.jsonl_object_key>)
Memory store contentsObject store (memory_stores.object_key_prefix + path)
Uploaded filesObject store (files.object_key)
Skill bundlesObject store (skill_versions.object_key)
Sandbox filesystemsmicrosandbox home (~/.microsandbox/) — ephemeral, checkpointed on idle
Raw vault secretsOnly ciphertext in Postgres; raw values in the microsandbox host-side store at runtime, purged on session end

The backend does not duplicate the conversation into Postgres (§28).

Object-store key layout

ArtifactKey
Session JSONL logtenants/<tenant_id>/sessions/<session_id>/log.jsonl
Memory store contentstenants/<tenant_id>/memory/<store_id>/<memory_path>
Uploaded filestenants/<tenant_id>/files/<file_id>
Skill bundlestenants/<tenant_id>/skills/<skill_id>/v<version>/
Sandbox snapshotstenants/<tenant_id>/snapshots/<session_id>/

JSONL durability policy (§28): active sessions append to local disk (Pi-native); the backend syncs to the object store on every session.status_idle transition and at a periodic interval while running (default 30s). A host loss can lose at most the tail of the current turn — never an idle session.

6. Retention & Purge

TableColumnPolicySource
sessionslast_activity_at30-day checkpoint window (idle sandbox retained); 90-day JSONL retention (purge-on-request supported)§6.3
memory_versionsexpires_at30-day retention; recent versions always kept regardless of age§13.5
idempotency_keysexpires_at24h replay window; rows swept after expiryapi-reference.md, §"Idempotency-Key"
job_runscreated_at90-day default (configurable)this doc
webhook_deliveriescreated_at90-day default (configurable)this doc
usage_recordsrecorded_at365-day default (configurable per tier)this doc
session_eventscreated_atFollows the owning session's retentionthis doc
rate_limit_bucketswindow_startTransient; rows expire with their windowthis doc

Per-tier overrides land in WP-5.5 (§29.6).

7. Migration Notes

  • Forward-only SQL migrations via node-pg-migrate (§3.2). No down migrations in prod; the -- Down Migration half exists for the up/down clean test and local development.
  • Each migration is a single .sql file in packages/backend/migrations/, numbered and applied in filename order.
  • Adding a migration means regenerating this doc (pnpm db:schema:gen) and committing the result. pnpm db:schema:check fails CI otherwise — that check is the control that makes a new table impossible to land unreviewed.
  • New tables must also be added to the TABLES list in packages/backend/src/infra/db/__tests__/migrations.test.ts, which is what makes the down-migration round-trip test able to catch a leaked table.
  • FKs reference tenant-scoped PKs. The tenantScoped(query) helper makes omitting the tenant_id filter a compile-time error and a runtime assertion failure (§27.1).