Migrations¶
Primer never runs migrations at startup. Applying them is an explicit step, so a rolling deployment cannot have several instances racing to alter the same schema.
Applying them¶
There are three, one per schema, and each runs from its own service directory:
for service in control-api chat retrieval; do
(cd "services/$service" \
&& PRIMER_DATABASE_URL=postgresql://user:pass@host/primer uv run alembic upgrade head)
done
All three must run. Retrieval's is easy to overlook because its schema holds
no Primer-defined tables — the vector integration creates its own table — but
nothing else creates the schema to put that table in, or the vector
extension its embedding column needs. Skip it and the retrieval service never
becomes ready, which shows up as an unhealthy container rather than as a
missing migration.
The Compose profile and the Helm chart both run all three for you: Compose as
one-shot migrate-* services, the chart as pre-install and pre-upgrade hooks.
The URL comes from the environment, never from a file in the repository, so no deployment's credentials live in version control. It may name an async driver — the same string the services use — and migrations will rewrite the scheme onto a synchronous one.
Privileges¶
CREATE EXTENSION needs rights an application role should not hold, so the
retrieval migration is where it happens. The running services never issue
DDL, which means the role they connect as does not need to be able to.
Checking where you are¶
Rolling back¶
Every migration has a tested downgrade, verified by a round trip against a real PostgreSQL in CI. A migration that cannot be rolled back cannot be safely deployed.
Downgrades do not drop the control schema itself, because it holds
Alembic's own version table.
Schema layout¶
Control's tables live in a dedicated control schema, Chat's in chat, and
Retrieval's vectors in vectors. No service's migrations touch another's
tables, so they can share one PostgreSQL instance without coordinating
releases.
Drift¶
CI asserts that the migrations and the models describe the same schema. A model changed without a migration fails the build rather than surfacing in production as a query against a column that was never created.
The vector schema is deliberately outside this check. Its table belongs to the vector integration, so there are no Primer models to compare against, and a hand-written copy of that table here would drift silently the first time the integration changed a column.