Skip to main content

Run database migrations

AgentArea’s schema is managed by Alembic and applied by the agentarea-api migrate command. Both deployment targets run it automatically before the API starts, so most of the time there is nothing to do. This guide covers the cases where there is: inspecting what ran, applying migrations by hand, and recovering a database that Alembic will not advance. Migrations run from agentarea-platform/apps/api, never from the repository root. alembic.ini sets script_location = alembic as a relative path, so Alembic resolves the migration directory against the working directory. Both the Kubernetes Job and the Compose service set working_dir: /app/apps/api for this reason.

Prerequisites

  • A reachable PostgreSQL instance with the agentarea database created
  • For local work: Python 3.12 or later and uv, from a clone of the repository
  • The same DATABASE_URL or POSTGRES_* values the platform uses

Steps

1. Understand what runs automatically

Both are ordered ahead of the API. Compose declares app_migrations: condition: service_completed_successfully as a dependency of app, so a failed migration keeps the API from starting rather than letting it serve against a stale schema. On Kubernetes the Job has ttlSecondsAfterFinished: 300 and disappears five minutes after it succeeds. agentarea-api migrate does more than alembic upgrade head:
  1. It opens a connection and runs SELECT 1, printing Database connection successful.
  2. It reads the current Alembic revision.
  3. If there is no revision and the provider_specs table exists, it treats the schema as already current and runs alembic stamp head instead of replaying migrations. This is the path for a database created by an older bootstrap.
  4. If there is no revision and no provider_specs table, it runs alembic upgrade head from empty.
  5. If there is a revision, it runs alembic upgrade head normally.
On any exception it prints Migration failed: and exits 1.

2. Apply migrations by hand

When you need to run them outside the normal startup path — after restoring a backup, or against an external database the Job cannot reach. On Kubernetes, run a one-off pod from the API image:
Or re-run the chart’s own Job by reinstalling with only that Job enabled. Under Compose:
From a clone, against a database you can reach directly:

3. Inspect migration state

From agentarea-platform/apps/api:
The platform also exposes this as a command:
Inside a running container:

4. Create a migration

Only when changing the schema in code.
alembic.ini sets file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(slug)s, so new files are named by ISO-style timestamp — 20260729_1432_add_widget_table.py. Do not rename them into any other scheme. Always read the generated file before committing. Autogenerate does not detect every change, and it will happily drop a column it does not recognise.

5. Know which databases exist

One PostgreSQL instance holds several logical databases. Alembic owns only the first. Under Compose these are created by postgres_init, which is idempotent. On Kubernetes each has its own create-*-db-job.

Verify

Confirm the database is at the revision the code expects — the two commands must print the same identifier:
Check the migration run itself succeeded:
Expect Migrations completed successfully, or Stamped database to head revision when the schema pre-existed. On Kubernetes:
The Job should read 1/1. Confirm the schema is actually usable, not merely stamped:

Troubleshooting

The migration Job sits in Init:0/1 and logs Waiting for database.... Its wait-for-db init container polls nc -z <host> <port> and never times out. The host is global.database.host, or the bundled PostgreSQL service when that is empty. Verify the host resolves from inside the namespace and the port is reachable. Migration failed: with a relation already exists error. Alembic is replaying a migration against a schema that already has the object. This happens when a database has tables but no alembic_version row, and the auto-stamp path did not trigger because provider_specs was absent — a “dirty” database, in the command’s own words. Confirm the schema really is current, then stamp it:
Stamping tells Alembic the database is current without checking. If the schema is not in fact current, you have hidden the gap rather than closed it; take a backup first. Can't locate revision identified by '<hash>'. The database records a revision that does not exist in the image’s migration directory — usually a downgrade to an older image after a newer one migrated, or a branch whose migration was never merged. Roll forward to the image that contains the revision. Do not delete the alembic_version row to make the error go away; it strands the schema at an unknown point. alembic branches returns rows. Two migrations claim the same parent, typically from two branches merged without rebasing. Generate a merge revision:
Migrations work locally but fail in the container with FAILED: No 'script_location' key found. The command ran from the repository root. alembic.ini uses a relative script_location, so the working directory must be agentarea-platform/apps/api (/app/apps/api in the image). A helm upgrade leaves the API running an older schema. The migration Job is a normal resource, not a Helm hook — the chart notes this is deliberate, to avoid a dependency deadlock with the database. It therefore does not block the Deployment rollout. Check the Job completed before assuming the upgrade is done. See upgrades.