Beta . Ortha CMS is in public beta. Every plan is free until it ends.
Features Ortha CMS on GitHub

Migrations

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

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

Documents 0.13.1 Updated Edit this page Report a problem

On this page

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 sync

3. Generate and apply:

npm run generate -- --name=add_article     # = npx orthacms generate --name=add_article
npm run migrate                            # = npx orthacms migrate

Commit 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 generate runs drizzle-kit from the root.
  • No dbCredentials: generating only diffs code against the snapshot on disk. No database, no secret.
  • out, ContentPlugin’s migrations descriptor 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 its out directory.
  • 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 migrate
Applying 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/database before changing it. (An empty URL would make pg fall back to PGHOST or 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=0 is refused: Studio would report the port it was asked for, not the one it bound.
  • A non-loopback --host prints 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 start

See database and migrations.

Inside the Ortha monorepo

Contributors to the CMS itself use Nx targets inferred by the private @orthacms/nx plugin — adapters over the same implementation:

App commandMonorepo 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 migratenpx nx run server:db:migrate
npx orthacms studionpx nx run server:db:studio
npx orthacms content syncnpx 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.