Database
Agent Zero's runtime is deliberately persistence-free. The repository contains exactly one database — Postgres, used by the dashboard's authentication session store — and one package that owns it.
Persistence boundary
Postgres has one owner: packages/database. It declares the tables in Drizzle (src/schema/), opens the connection pool (src/client.ts), and keeps the migrations as reviewable, checked-in SQL under drizzle/. Nothing else constructs a client or names a column.
The split from packages/auth is the same rule applied one level down. Authentication policy and the store it happens to use change for different reasons and are reviewed by different eyes: packages/database knows nothing about sign-in, sessions, or invitations beyond the shape of the rows, and packages/auth states what is allowed and hands drizzleAdapter a client it did not open. The dependency runs one way, and packages/database must never import packages/auth.
Declaring the schema here rather than letting Better Auth's own migration CLI generate it is what makes user, session, account, and verification diffable like any other change. Column names are load-bearing: the Better Auth adapter maps its models by name, so a rename that type-checks can still break sign-in at runtime. Migrations are generated (db:generate) and applied (db:migrate) from the database package alone, and createDatabase is a factory rather than a module-level singleton so importing the package never opens a socket — the composition root owns the pool's lifetime.
DATABASE_URL is the connection string a deployment sets. AUTH_DATABASE_URL is still accepted, because the store used to live inside packages/auth and a deployment configured before the split must not fail to start on upgrade.
The same rule covers the tables a Better Auth plugin brings with it. invite and invite_use (Better Enrollment) are declared here alongside organization and member, so a deployment that never enables the feature still migrates them and simply never writes to them — the alternative is a schema that depends on which flags were set when the migration ran. Two of their columns are deliberate exceptions to the repository's usual habits: an accepted invitation is a permanent audit record, so it carries no foreign key to the organization, team, or account it names and denormalizes inviter_name/inviter_email in order to survive their deletion; and invite_use carries only used_at rather than the shared timestamp pair, because a use is a point-in-time fact that must not look rewritable.
Setup
Point DATABASE_URL at a Postgres database, then apply the schema once before the first run:
aube run db:migrate
db:generate only needs to run again after editing packages/database/src/schema/:
aube --filter @agent-zero/database run db:generate
AUTH_DATABASE_URL is still read when DATABASE_URL is unset, so a deployment configured before the store moved into packages/database keeps starting; prefer DATABASE_URL for new ones.
Task history is not in Postgres
Task history persists through a narrow KeyValueStorage contract backed by the ViteHub KV Runtime Helper (registered from apps/dashboard/nuxt.config.ts): filesystem-backed fs-lite by default, with Cloudflare KV, Deno KV, or Upstash dropping in as driver configuration without touching application code. A hosted deployment preset resolves the host's own driver instead of the filesystem one, so a Vercel deployment needs KV_REST_API_URL and KV_REST_API_TOKEN for its Upstash store.
Records are redacted before they are written and never contain review input or checkout paths, so task history cannot become a credential or filesystem leak.
Safety model
Agent Zero is built so that the safe path is the default path and every escalation is explicit, auditable, and reversible.
CLI
The zero CLI is the local entry point: it parses arguments with @bomb.sh/args and renders with @clack/prompts. Inside the repository, run it as aube run zero <command>.