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_subtitleThis 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.ts —
npx 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 migrateOne 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 studioOpens 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.