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
| Schema | Owner | Purpose | Key tables |
|---|---|---|---|
identity | agent-identity | Agents, device keys, secrets, delegations, tunables | agents, device_keys, secrets, delegations, settings |
postbox | agent-postbox | Mailboxes, messages, rules, suppressions, webhooks | addresses, messages, inbound_rules, outbound_suppressions, inbound_webhooks, domains |
recall | agent-memory | Episodic + semantic memory, working sets, KV | episodic, semantic, working_sets, kv |
mesh | agent-mesh | WireGuard peers, ACLs, PSKs | peers, acls, psk |
gateway | agent-gateway | Hostnames, cert ledger, IP history | hosts, certs, ip_history |
scheduler | agent-scheduler | Jobs and runs | jobs, runs |
artifacts | agent-artifacts | File registrations, grants | artifacts, grants |
governor | agent-governor (new) | Policy, decisions, budgets | policies, decisions, budgets |
audit | cross-cutting | Hash-chained event ledger | events, 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 searchivfflatonembedding— similarity search.mesh.peers (agent_id)andmesh.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:
| Key | Type | Default | Meaning |
|---|---|---|---|
mesh_peer_retention_days | int | 30 | Drop peers whose last_seen is older. |
postbox_retention_days | int | 90 | Hard-delete sent/read messages past this. |
recall_episodic_retention_days | int | 180 | Trim old turns. |
artifacts_ttl_days | int | 30 | Default expiry for newly registered artifacts. |
See the per-subsystem pages for the full list.