Company Agents stores everything in Postgres: companies, teams, agents, tasks, runs, leases, costs, memory scopes, audit trail, secrets (encrypted), integration state. There are two flavors of Postgres you can run against.

PGlite (default for desktop)

PGlite is Postgres compiled to WebAssembly, running inside the orchestrator process. It writes to a directory on disk and serves SQL without a separate network hop. It is real Postgres, not a SQLite-style compromise, which means the schema, the queries, and the migrations are identical to the real-Postgres case. Data lives at:
~/.company-agents/instances/default/db/
├── base/               # the PGlite data directory
├── pg_wal/
├── pg_stat/
└── ...
Everything is one process. There is no TCP port, no credentials, no connection pool. Backup is “copy the directory while the orchestrator is stopped.” PGlite is fine for:
  • A single user on a single machine
  • Running a handful of companies
  • Workloads below a few hundred concurrent tasks
PGlite starts to feel slow when:
  • You have thousands of runs in the audit trail
  • You run dozens of concurrent tasks
  • You are generating large activity logs
Those are the symptoms that mean it is time to switch to real Postgres.

Real Postgres

For real Postgres, point the orchestrator at a connection URL:
DATABASE_URL=postgres://company-agents:company-agents@localhost:5432/company-agents
The orchestrator detects DATABASE_URL and uses real Postgres instead of PGlite. Everything else (schema, migrations, queries) is identical. Supported:
  • Postgres 15 or 16 (we test against 16)
  • Any standard Postgres distribution: the official Docker image, RDS, Cloud SQL, Neon, Supabase, Railway, Fly Postgres, whatever you prefer
  • TLS via PGSSLMODE=require and friends
Not supported:
  • Postgres 14 or older (missing features we rely on)
  • Aurora Postgres compatibility mode (close but not exact, we have hit edge cases)

Schema

The schema is defined in packages/db/src/schema/ using Drizzle. There is one file per domain: companies.ts, agents.ts, tasks.ts, runs.ts, leases.ts, costs.ts, memory.ts, audit.ts, secrets.ts, integrations.ts. Key tables you will care about:
  • companies — one row per company
  • teams — one row per team, foreign key to companies
  • agents — one row per agent, foreign key to teams
  • tasks — the task queue, with state machine columns
  • runs — one row per run, foreign key to tasks
  • leases — the active lease table; auto-expired by the lease reaper
  • cost_entries — every metered cost, for roll-ups
  • audit_events — the audit trail, append-only
  • secrets — encrypted credential store
The full schema is visible in the source; the README in packages/db/ has an ER diagram if you prefer pictures.

Migrations

Migrations are Drizzle migrations. They live at packages/db/drizzle/ and are numbered sequentially. Run migrations manually:
pnpm db:migrate
The Docker image runs migrations on container start. The desktop app runs them on app start. You rarely have to think about this unless you are hacking on the schema itself. To generate a new migration after changing a schema file:
pnpm --filter @company-agents/db generate
This produces a new numbered SQL file you commit with your change.

Backups

PGlite

# Stop the orchestrator first
cp -r ~/.company-agents/instances/default/db \
      ~/.company-agents/backups/db-$(date +%F)
You can also use the dashboard’s built-in backup:
company-agents backup create
which produces a .tar.gz of the data directory plus the secrets and the runs directory.

Real Postgres

Use whatever your Postgres provider gives you. pg_dump/pg_restore for self-managed, snapshots for managed. The schema is standard Postgres; there is nothing Company Agents-specific about how you back it up. We recommend:
  • Hourly incremental backups (WAL archiving if you manage it yourself)
  • Daily full dumps retained for 30 days
  • Weekly full dumps retained for a year
  • A restore drill at least quarterly (a backup you have never tested is not a backup)

Switching from PGlite to real Postgres

  1. Start your real Postgres (docker, managed, whatever)
  2. Stop the orchestrator
  3. Export from PGlite:
company-agents db export --format pg-dump \
  --output company-agents-export.sql
  1. Import into real Postgres:
psql postgres://company-agents:company-agents@localhost:5432/company-agents \
  < company-agents-export.sql
  1. Set DATABASE_URL and restart the orchestrator
  2. Verify the dashboard loads and your companies are intact
  3. Delete the old db/ directory only after verifying the new install for at least a day

Switching from real Postgres to PGlite

Reverse of the above. The exporter supports both directions. This is rare (why would you?), but it is there for development and disaster recovery.

Connection pool

The orchestrator uses a connection pool sized by the number of concurrent workers it expects to run. The default is fine for most deployments. If you run very high concurrency, tune:
DATABASE_POOL_MIN=5
DATABASE_POOL_MAX=30
Do not tune without measuring. Over-pooling hurts more than it helps.

Next

  • Secrets for how the encrypted secret store interacts with the database
  • Storage for the other on-disk state (runs, workspaces, backups)
  • Environment variables for all database-related env vars