embedded-postgres — not WASM-based PGlite. Drop-in compatibility with regular Postgres clients, dump tools, and migrations.
1. Embedded PostgreSQL (Default)
Zero config. If you don’t setDATABASE_URL, the server starts an embedded PostgreSQL instance automatically.
- Creates
~/.aoa/instances/default/db/for storage - Ensures the
aoadatabase exists (database NAME inside Postgres is wire-compat with the embedded cluster bootstrap, separate from the AoA brand) - Runs migrations automatically
- Starts serving requests
rm -rf ~/.aoa/instances/default/db.
The Docker quickstart also uses embedded PostgreSQL by default.
2. Docker Compose PostgreSQL
The default Docker Compose stack runs AoA with a durable PostgreSQL database that includes pgvector:DATABASE_URL=postgres://aoa:aoa@db:5432/aoa automatically. The database is not published on localhost:5432 by default; it is only reachable by services on the Compose network.
Open a psql shell with:
The server does NOT auto-load a repo-root.env. It only reads.envfrom the AoA config directory — the directory containing the activeconfig.json(default~/.aoa/instances/default/, or the nearest.aoa/config.jsonfound by walking up from the working directory; seeserver/src/paths.tsandserver/src/config.ts). Copying.env.exampleto the repo root has no effect. Either setDATABASE_URLinline / export it in your shell, or place the.envin the config directory.
drizzle-kit push for an existing or production database. It skips
the migration journal, data backfills, and migration safety gates. Use
pnpm db:migrate for upgrades.
3. Hosted PostgreSQL (Supabase)
For production, use a hosted provider like Supabase.- Create a project at database.new
- Copy the connection string from Project Settings > Database
- Set
DATABASE_URLin your.env
Migrations in multi-replica cloud deployments
On boot the server auto-applies any pending migrations (applyPendingMigrations in packages/db/src/client.ts). In a horizontally
scaled cloud deployment, run migrations as a single init-job / one-shot
container that finishes before the application replicas start, rather than
relying on every replica’s boot-time auto-apply. Two replicas booting at once
can otherwise observe the same pending set and race non-idempotent DDL (an
ADD COLUMN / ADD CONSTRAINT without IF NOT EXISTS).
As defense-in-depth, applyPendingMigrations holds a session-level
PostgreSQL advisory lock (pg_advisory_lock(hashtext('aoa:migrations')))
across the whole inspect-and-apply, so if two replicas do auto-apply
concurrently only one runs the migrator; the other waits, re-inspects under the
lock, and finds the schema already up to date. This is a safety net, not a
substitute for a single migration job.
The init job must set AOA_DEPLOYMENT_MODE=cloud_auth. Migration 0188 is a
one-way multi-tenant cutover for populated databases, so the shared migrator
refuses to apply it until a full database snapshot has been taken and "0188"
has been recorded in the canonical instance_settings row where
singleton_key = 'default', under general.migrationSnapshots. Record and
verify the marker explicitly before starting the migration job:
"0188". If it does not, stop instead of applying migration
0188. Keep every application replica stopped from this marker operation until
the migration job completes, so no settings writer can race the cutover. If the
manual migrator cannot determine the deployment mode, it also fails closed for
that populated 0188 case instead of assuming a trusted local deployment.
Migration 0200 adds process-owner provenance for persisted local runtime
PIDs. Drain and stop every pre-0200 application binary before starting the
new replicas: old binaries do not understand the owner column and can still
probe or signal foreign numeric PIDs. The nullable column is intentionally not
backfilled because legacy PID ownership cannot be inferred safely. In
cloud_auth, an ownerless legacy PID row blocks startup until an operator has
stopped the process on its original host and cleared the persisted PID. Trusted
single-owner modes preserve legacy/foreign rows but skip desired-service
auto-restart until those rows are remediated, preventing duplicate detached
commands and port conflicts.
The same migration changes adapter-managed runtime IDs from globally trusted
adapter values to company-namespaced hashes. As a one-time cutover it retires
all pre-0200 adapter_managed runtime rows, marks their task outputs stopped,
and clears their preview URLs before deleting the old rows. Active adapters
publish fresh company-scoped rows on their next report. Expect a brief preview
refresh after deployment; old preview links intentionally stop working.
While any PID-bearing local_process row remains in the shared database,
every cloud_auth replica must have its own explicit, unique
AOA_RUNTIME_PROCESS_OWNER_ID; a replica with the variable unset refuses to
boot because it cannot safely distinguish its own prior process rows from a
foreign host’s. Normal production cloud deployments should remediate these
rows and leave the unsandboxed execution override disabled.
Offline remediation for migration 0200 runtime rows
Use the runtime-service IDs printed in the startup error. First stop the old AoA binary and confirm on the original process host that each recorded PID and its process group/tree are gone. Do not use the SQL below as a process-stop mechanism: it only repairs tracking after OS-level termination has been independently confirmed. Take a database backup, substitute the exact UUIDs, and inspect the locked rows before updating them:provider_ref IS NULL, then start exactly one new
application replica first. In a rolling deployment, never overlap pre-0200
binaries with the new version.
If using connection pooling, disable prepared statements:
Switching Between Modes
The Drizzle schema (
packages/db/src/schema/) is the same regardless of mode.
Marketplace recovery operation ledger
Fleet marketplace reconciliation uses themarketplace_reconciliation_operations table as an instance-scoped,
restart-durable operation ledger. Its primary key prevents operation-ID reuse,
and its partial unique index plus owner-scoped lease prevents two application
replicas from running different fleet reconciliations concurrently. Company
activity_log rows remain audit detail; they are not the operation lock or
terminal-state authority.
Backups
AoA includes a built-in backup utility inpackages/db/src/backup-lib.ts. It exports runDatabaseBackup and runDatabaseRestore.
Scope
Backups include all non-system schemas —public, drizzle, and any other user-created schemas. This means the Drizzle migration journal (drizzle.__drizzle_migrations) is always included, so restored databases reflect the correct migration state and never enter a “pending migrations” limbo.
System schemas (pg_catalog, information_schema, pg_toast*, pg_temp_*) are excluded.
Backup engine
runDatabaseBackup selects an engine automatically (backupEngine: "auto"):
- pg_dump — used when no
excludeTables/nullifyColumnstransforms are configured. Produces a gzip-compressed SQL file. - JavaScript — fallback when pg_dump is unavailable or when transforms are required. Uses
COPY … FROM stdinfor bulk tables (requires psql on restore) orINSERTstatements whenbackupEngine: "javascript"is explicitly set.
Retention policies
Two modes:
The default policy (
DEFAULT_BACKUP_RETENTION) keeps 7 daily, 4 weekly, and 1 monthly backup.
Restore
runDatabaseRestore tries psql first. If psql is unavailable and the backup file contains AoA statement breakpoints, it falls back to the JavaScript runner (statement-by-statement via the postgres client). Legacy backups without breakpoints require psql.