Database¶
Trimus stores everything except uploaded files in postgres. This page covers the operator-facing parts: setup, migrations, backups, connection-string tuning, and what to do when things go sideways.
Required version¶
PostgreSQL 16 or later with the pgvector extension available.
Earlier versions might appear to work but are not tested.
The migrations rely on:
vector(pgvector) — required by migration 103 (note_embeddings). TheCREATE EXTENSION IF NOT EXISTS vectorwill fail at boot if the extension isn't installed in the server, so use a Postgres image/build that ships pgvector.pg_trgm— fuzzy issue search; auto‑created by migration 001 (the extension must be available).gen_random_uuid()(pgcrypto) — also available in core Postgres 16.JSONBandFOR UPDATE SKIP LOCKED— standard Postgres‑16 features (the worker pool claims jobs withSELECT … FOR UPDATE SKIP LOCKED).
Setting up postgres¶
Local development¶
The shipped docker-compose.yml runs pgvector/pgvector:pg16 — a
drop‑in superset of stock postgres:16 with pgvector preinstalled:
That's it. Set DATABASE_URL=postgres://trimus:trimus@localhost:5432/trimus_dev?sslmode=disable
in your environment.
Production¶
Run postgres on its own host (or a managed service like RDS, Cloud SQL, Supabase, Neon). Trimus is happy with any of them that can load pgvector — RDS, Cloud SQL, Supabase, and Neon all support the extension; confirm it's enabled before first boot.
Minimum sizing for a small team:
- CPU: 2 vCPU
- RAM: 4 GB
- Disk: 20 GB SSD, growing with run-event retention
Heavy heartbeat traffic stresses heartbeat_runs and run_events
tables. Bump RAM and disk before you bump CPU.
Connection string¶
Recommended for production:
sslmode=requireminimum.sslmode=verify-fullif you have the CA bundle (most managed providers offer this — setsslrootcert=path/to/ca.pem).- Pin a connection-pool size with the
pgbouncer-friendly format if you front postgres with PgBouncer.
Username permissions: Trimus owns its schema. The user in
DATABASE_URL needs CREATE / SELECT / INSERT / UPDATE / DELETE on
all Trimus tables. The simplest setup is to make Trimus the owner of
the database.
Migrations¶
Migrations live at internal/db/migrations/<NNN>_<name>.sql (176 files
as of v5.0.2; highest is 176_worker_shared_models.sql),
managed by goose.
The server applies migrations on boot
trimus-server runs db.RunMigrations at startup before the
pool is opened, applying every pending migration up to the binary's
latest. So an ordinary upgrade is just "restart the server" — you
do not need a separate migration step in production. See
upgrades.md.
trimus-seed is the dev seeder. It also runs goose up (so it's a
convenient way to migrate a dev database) and then inserts default
fixtures only if they don't already exist — idempotent, safe to re‑run:
For migrations only, from a source checkout:
What's in trimus-seed's seed step¶
A default user, a sample company, two sample agents, three sample issues. Useful for poking at a fresh install. Skip the seed in production by writing a tiny migration-only wrapper, or just delete the seed company after first run.
Migration ordering¶
Migrations are forward-only; goose down is supported but not
expected. Each migration has both -- +goose Up and -- +goose
Down blocks so reverts are technically possible — but the
operational expectation is "always roll forward". See
upgrades.md for upgrade strategy.
Backups¶
See backup-and-restore.md for the
complete operator runbook — pg_dump is only one of four
components a full Trimus backup must capture (the other three are
TRIMUS_HOME, TRIMUS_STORAGE_DIR, and the
TRIMUS_ENCRYPTION_KEY). That document also covers the cold
restore + verification procedure, common pitfalls, and a
quarterly verification cadence.
A 30-second summary lives there. The TL;DR for postgres alone:
But that alone is not a complete backup. Read the linked doc.
Routine maintenance¶
VACUUM / ANALYZE¶
Postgres autovacuum is on by default. Trust it. Only intervene if you see bloat reports or your hosting provider warns you.
Heavy tables to watch¶
heartbeat_runs— one row per run. Grows fast. Terminal rows are pruned byTRIMUS_HEARTBEAT_RUN_RETENTION_DAYS(default 90; setTRIMUS_HEARTBEAT_RUN_RETENTION=disableto opt out).heartbeat_run_events— many rows per run; the streaming event log. Grows fastest. Pruned for succeeded runs byTRIMUS_RUN_EVENTS_RETENTION_DAYS(default 30); failed‑run events are retained indefinitely.ON DELETE CASCADEfromheartbeat_runsmeans reaping a parent run also reaps its events.activity— append-only audit log. Slow but steady growth.comments— humans + agents, slow growth.documents+document_revisions— growth depends on how often agents revise documents.
If postgres is creaking under one of these, the levers are:
- Lower
TRIMUS_RUN_EVENTS_RETENTION_DAYSand/orTRIMUS_HEARTBEAT_RUN_RETENTION_DAYS. - Use the Trash UI to soft-delete and then hard-delete old issues / projects / agents.
pg_dump+ restore into a fresh DB if you really need to compact.
Connection pool tuning¶
trimus-server uses pgx's connection pool. Default settings are
pgx's defaults — fine for most workloads. Overrideable via the
DATABASE_URL query parameters:
Watch for:
- Connection-exhausted errors under heavy heartbeat load → bump
pool_max_conns. - Postgres connection-limit-reached errors → check
max_connectionson the postgres side; lowerpool_max_conns× number-of-trimus-instances if you're hitting it.
Common problems¶
"DATABASE_URL is required"¶
Set it. The server refuses to start without one.
"column 'xxx' does not exist"¶
Migrations are out of date — but the server applies them on boot, so
this usually means the old binary is still running. Restart
trimus-server on the new binary (or run trimus-seed / the goose
incantation) against this database. Check the applied version with:
Compare against the highest file in internal/db/migrations/ (176 as
of v5.0.2).
"extension \"vector\" is not available"¶
The Postgres server doesn't have pgvector installed. Use the
pgvector/pgvector:pg16 image (or enable the extension on your managed
provider) and restart. Migration 103 runs CREATE EXTENSION … vector.
"duplicate key value violates unique constraint" on startup¶
Probably running two trimus-seed instances simultaneously, or the
seed is running while the server is also running. Run seed once,
serially.
Cross-company queries returning unexpectedly empty results¶
Trimus deliberately returns 404 (not 403) on cross-company access. If your admin queries look empty, check that you're scoped to the right company.
Where to next¶
- storage.md — files alongside the database.
- upgrades.md — migration strategy when bumping versions.
- monitoring.md — keeping an eye on postgres + the app.