Features Apograph CMS on GitHub

Migrations

Content plugin @apograph/content-server@apograph/content-admin

Per-plugin migrations, generated from your schema and applied by the host in one command.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

Every plugin owns its own schema and ships its own migrations, tracked in its own table. The host applies all of them.

That includes your content types: the generated content_<name> tables belong to the host application, which is why apps/server has its own drizzle.config.ts and its own migrations/ directory.

Generating

npx apograph generate --name=add_article_subtitle

This diffs your schema against the committed snapshot and writes SQL into the project’s migrations/ directory. Commit the generated file alongside the change to the content type — they are one change and should travel together.

Generation needs no database and no secrets. It only compares your code to the snapshot on disk.

Inside the Apograph monorepo the command is different

An app scaffolded by create-apograph-app has no Nx and runs apograph generate. A contributor working in the CMS monorepo runs the Nx target the private @apograph/nx plugin infers on any project with a drizzle.config.tsnpx nx run server:db:generate --name=<change> for the host’s content tables, or npx nx run identity-server:db:generate --name=<change> for a plugin that owns tables. Both are adapters over one implementation.

A table you did not export is a table drizzle-kit cannot see

drizzle-kit diffs top-level table exports from the file named by drizzle.config.ts. A content type whose table is not re-exported — or a many-relation whose join table is not — simply will not appear in the diff, and the type will fail at runtime against a table that was never created. Use joinTableOf, which throws when the relation is missing or renamed rather than letting the table vanish from the migration quietly.

Applying

npx apograph migrate

One command applies every plugin’s pending migrations, each under its own tracking table. It compiles the server first and reads the compiled config and plugin list, so it needs no TypeScript loader at runtime. (In the monorepo, the equivalent is npx nx run server:db:migrate, inferred on the project with an apograph.config.ts.)

It refuses to run without a database URL, and names the host:port/database it is about to change before doing so. That refusal is worth having: an empty connection string makes pg fall through to PGHOST/PGUSER/PGDATABASE, or to localhost and the OS user — which used to mean a cheerful “Migrations complete” after building an entire schema in a database nobody had named.

Order matters

Migrations are applied in exactly the order buildPlugins() returns the plugins, and that order is load-bearing but not declared anywhere: workspaces’ membership table has a foreign key into identity’s users, so identity has to go first.

If a migration fails, the error names the plugin, says how many committed before it, and points at plugin order as the usual cause.

Inspecting

npx apograph studio

Opens Drizzle Studio against the host database, introspecting the live schema. Takes optional --host and --port to change where it binds. (Monorepo: npx nx run server:db:studio.)

Why generation is never cached

This one is for contributors to the monorepo: db:generate is deliberately not a cacheable Nx target, and it is worth knowing why if you are wondering whether Nx is misconfigured.

Every project’s schema location comes from its own drizzle.config.ts, and drizzle-kit diffs against migrations/meta/*_snapshot.json — which sits inside what would be the declared output directory. No input glob can be correct, and the result is not a function of its inputs anyway. A cache hit would both skip a generation somebody asked for and restore migrations/ over the working tree.

In production

For a deployment, the committed SQL is the artefact — run apograph migrate in your release pipeline against the production DATABASE_URL, before the new server version starts. It builds first, so the pipeline needs the dev dependencies present at that step.

See database and migrations for the operational side.

Generated SQL is a starting point, not a guarantee

drizzle-kit generates a diff. It does not know that renaming a column is a rename rather than a drop-and-add, and it does not write data migrations. Read the generated SQL before committing it — particularly for renames, type changes and anything with a NOT NULL on an existing table.