Every plugin ships its own migrations with its own tracking table. Your content
tables — content_<name>, their join tables and content_entry_revisions —
belong to the host app, so the app has its own drizzle.config.ts and
migrations/ directory.
First content type, start to finish
A new app already has the plumbing: the empty manifest
apps/server/src/content/index.ts, a migrations descriptor on
ContentPlugin, apps/server/drizzle.config.ts, and a baseline migration in
apps/server/migrations/ that creates content_entry_revisions. Adding a type
edits none of it.
1. Write the type — apps/server/src/content/collections/article.ts,
exporting a collection named article. See
registering a type.
2. Regenerate the manifest:
npm run content:sync # = npx orthacms content sync3. Generate and apply:
npm run generate -- --name=add_article # = npx orthacms generate --name=add_article
npm run migrate # = npx orthacms migrateCommit the type file, the manifest and the generated SQL (with
migrations/meta/) together.
Or let the schema builder do all three
In development, with SCHEMA_BUILDER=true, the admin’s Content Model page
writes the type file and the manifest, generates the migration, applies it in
one transaction and lets the dev watcher restart the server. The files it
leaves are the same ones, to commit the same way. A draft that removes
something gets two migrations — <name>_removals first, then the rest. See
the schema builder.
The drizzle config a generated app ships:
// apps/server/drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './apps/server/src/content/index.ts',
out: './apps/server/migrations'
});- Paths are relative to the app root, not the config file —
orthacms generateruns drizzle-kit from the root. - No
dbCredentials: generating only diffs code against the snapshot on disk. No database, no secret. out,ContentPlugin’smigrationsdescriptor and the schema builder’s default all name one folder. Move one and move all three.
Generating
npx orthacms generate --name=add_article_subtitle- Diffs
apps/server/drizzle.config.ts’s schema against the committed snapshot and writes SQL to itsoutdirectory. - Without that config file the command stops and says so.
drizzle-kit only sees top-level table exports
A type whose table — or a many-relation whose join table — is not re-exported
from the schema file is missing from the diff, and fails at runtime against a
table that was never created. orthacms content sync writes every one of them
into the manifest; run it after adding, renaming or removing a type file.
Read the SQL before committing
drizzle-kit writes a diff. It cannot tell a rename from a drop-and-add, and it
writes no data migrations. Check renames, type changes (e.g. adding
integer: true) and any new NOT NULL on an existing table.
Applying
npx orthacms migrateApplying migrations for <n> plugin(s) to localhost:5432/ortha
Applying migrations: … → __drizzle_migrations_content
…
Migrations complete.- Applies every plugin’s pending migrations, each under its own tracking table.
- Builds the server first and reads the compiled config and plugin list — no TypeScript loader at runtime.
- Refuses to run without a database URL, and names the
host:port/databasebefore changing it. (An empty URL would makepgfall back toPGHOSTor localhost — a database nobody named.)
Order matters
Migrations run in the order buildPlugins() returns plugins. Workspaces has a
FK into identity’s users, so identity must come first.
On failure the error names the plugin and how many committed before it. Nothing is rolled back; fix the cause and re-run — it resumes at the failed plugin.
Inspecting
npx orthacms studio # Drizzle Studio on 127.0.0.1:4983
npx orthacms studio --port=5000--port=0is refused: Studio would report the port it was asked for, not the one it bound.- A non-loopback
--hostprints a warning — Studio is an unauthenticated read/write console on your database.
In production
Run orthacms migrate in the release pipeline against the production
DATABASE_URL, before the new server starts. It builds first, so that step
needs dev dependencies installed.
DATABASE_URL=postgres://… npx orthacms migrate && npx orthacms startInside the Ortha monorepo
Contributors to the CMS itself use Nx targets inferred by the private
@orthacms/nx plugin — adapters over the same implementation:
| App command | Monorepo equivalent |
|---|---|
npx orthacms generate --name=<n> | npx nx run server:db:generate --name=<n> |
| (a plugin’s own tables) | npx nx run @orthacms/identity-server:db:generate --name=<n> |
npx orthacms migrate | npx nx run server:db:migrate |
npx orthacms studio | npx nx run server:db:studio |
npx orthacms content sync | npx nx run server:content:sync |
db:generate is deliberately not cached: drizzle-kit’s snapshot lives inside
the output directory, so no input glob is correct, and a cache hit would skip a
requested generation and restore migrations/ over your working tree.