Skip to main content

Data model

Nine schemas, each owned by one subsystem. Edge functions pin their client to a specific schema so cross-schema reads are explicit and auditable.

Schema map

SchemaOwnerPurposeKey tables
identityagent-identityAgents, device keys, secrets, delegations, tunablesagents, device_keys, secrets, delegations, settings
postboxagent-postboxMailboxes, messages, rules, suppressions, webhooksaddresses, messages, inbound_rules, outbound_suppressions, inbound_webhooks, domains
recallagent-memoryEpisodic + semantic memory, working sets, KVepisodic, semantic, working_sets, kv
meshagent-meshWireGuard peers, ACLs, PSKspeers, acls, psk
gatewayagent-gatewayHostnames, cert ledger, IP historyhosts, certs, ip_history
scheduleragent-schedulerJobs and runsjobs, runs
artifactsagent-artifactsFile registrations, grantsartifacts, grants
governoragent-governor (new)Policy, decisions, budgetspolicies, decisions, budgets
auditcross-cuttingHash-chained event ledgerevents, monitor

Ownership: the agent_id column

Every user-scoped table carries an agent_id text not null that acts as the tenancy key. Indexes are composite (agent_id, ...) so range scans stay tenant-local. Functions that take p_agent_id use it both as a filter and a guard: returning zero rows is not "nothing matched" but "you asked about someone else's data."

Security Rules policy

Every table has two default policies:

create policy deny_all_anon
on <schema>.<table> for all to anon using (false) with check (false);
create policy deny_all_auth
on <schema>.<table> for all to authenticated using (false) with check (false);

Reads and writes only happen through the service_role that Cloud Functions use. Agents never hit Firestore directly, and the anon JWT path is dead on arrival.

Hash-chained audit

create table audit.events (
id bigserial primary key,
occurred_at timestamptz not null default now(),
actor text not null,
category text not null check (category in ('auth','memory','mail','mesh','gateway','admin')),
action text not null,
subject text,
payload jsonb not null default '{}'::jsonb,
prev_hash bytea,
row_hash bytea not null
);

row_hash = sha256(prev_hash || occurred_at || actor || category || action || subject || payload). audit.verify_chain() recomputes from id 1 forward and returns the first mismatch. The daily agentpack_audit_monitor cron stamps audit.monitor(last_run_at, last_total_rows, last_first_bad_id) so an ops dashboard can alert on divergence.

Critical indexes

A handful of indexes carry the platform's performance. Most of them come from migration 0068's advisor fixes:

  • postbox.messages (agent_id, created_at desc) — the inbox listing path.
  • recall.semantic + Firestore vector search ivfflat on embedding — similarity search.
  • mesh.peers (agent_id) and mesh.psk (agent_lo, agent_hi) — netmap fanout.
  • gateway.hosts (agent_id, host) unique — one host per (agent, fqdn).
  • scheduler.runs (agent_id, job_name, started_at desc) — per-job timeline.

Tunables via identity.settings

Operator-adjustable knobs live in identity.settings (key jsonb, value jsonb) and are surfaced by /settings/list and /settings/set. Today's keys include:

KeyTypeDefaultMeaning
mesh_peer_retention_daysint30Drop peers whose last_seen is older.
postbox_retention_daysint90Hard-delete sent/read messages past this.
recall_episodic_retention_daysint180Trim old turns.
artifacts_ttl_daysint30Default expiry for newly registered artifacts.

See the per-subsystem pages for the full list.