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

Command line

The two binaries an installed app is built, run and migrated with.

Documents 0.13.1 Updated Edit this page Report a problem

On this page
BinaryJob
create-orthacms-appScaffolds a new app.
orthacmsBuilds, runs and migrates an app. A devDependency of every generated app.

Scaffolding an app

npx create-orthacms-app my-cms
# fully non-interactive:
npx create-orthacms-app my-cms --yes --media media-s3 --protocols graphql,mcp --mail mail-smtp
FlagValuesDefault
--yesAccept every default, ask nothing—
--media <id>media-local, media-s3, media-azure, media-gcs, media-vercel-blobmedia-local
--copilot <ids>copilot-anthropic, copilot-openai (comma-separated), or nonenone
--sso <ids>sso-oidc, sso-github, sso-saml (comma-separated), or nonenone
--protocols <ids>graphql, mcp (comma-separated), or none — REST is always onREST only
--mail <id>mail-smtp or nonenone
--no-installSkip npm install—
--no-gitSkip git init—
-h, --helpUsage—
  • --yes or a non-TTY takes defaults without prompting, so CI never hangs.
  • Ids are the full ids (media-s3), not s3.
  • Every @orthacms/* dependency is pinned to the scaffolder’s own version: npx create-orthacms-app@0.7.0 generates a 0.7.0 app. Nothing resolves latest.
  • 0.7.0: a generated app installs on npm 10 (Node 22’s bundled npm). Earlier versions could crash in npm install with Cannot read properties of null (reading 'edgesOut').

After it finishes:

cd my-cms
docker compose up -d
npm run migrate
npm run dev        # → http://localhost:4200

The interactive questions

QuestionKindDefault
Where should uploads be stored?one ofLocal filesystem
AI copilot — which model backends?any ofnone — the copilot stays off
Single sign-on — which identity providers?any ofnone — email and password
Which protocols should the content API speak?any ofREST (locked on)
How should the app send invitations and password resets?one of, or noneDo not configure

Multi-selects default to nothing extra: a hosted copilot sends content to a third party, and an unrequested endpoint is still an endpoint.

Mail. mail-smtp installs @orthacms/mail-server and @orthacms/mail-provider-smtp, writes config/mail.ts, a plugins.mail field in orthacms.config.ts, a mailPlugin() helper in plugins.ts, MAIL_* and SMTP_* keys in .env, and a README section. MAIL_PROVIDER is empty in a fresh app, so it boots before you have a relay. mail-domain, mail-provider-console and mail-provider-testkit are installed in every app. See sending mail.

Content. Every app starts with no content types but with their plumbing: the empty manifest src/content/index.ts, collections/ and pages/, drizzle.config.ts, and a baseline migration under apps/server/migrations. The schema builder is installed and registered on both sides, with SCHEMA_BUILDER=false in .env.

Building and running

npx orthacms <command> [options]
CommandWhat it runsnpm script
orthacms devtsc --watch, node --watch and Vite togethernpm run dev
orthacms buildtsc for the server, Vite for the adminnpm run build
orthacms startThe built server, dist/server/src/main.jsnpm start
orthacms migrateBuilds the server, then applies every plugin’s migrationsnpm run migrate
orthacms generate [--name=<n>]A migration for your content tables, from apps/server/drizzle.config.tsnpm run generate -- --name=<n>
orthacms content syncRewrites the content manifest, apps/server/src/content/index.ts, from the type files beside itnpm run content:sync
orthacms studio [--host --port]Drizzle Studio on the app’s databasenpm run studio
FlagApplies to
--serverbuild, dev — server only
--adminbuild, dev — admin only. Both together is refused
--host, --portstudio. --port=0 is refused; a non-loopback host warns
--name=<n> or --name <n>generate
-h, --help, helpAnywhere; prints usage, exits 0. A bare orthacms does the same
-v, --versionThe installed version. Wins over --help
npx orthacms build --server
npx orthacms generate --name=add_article_subtitle
npx orthacms --version

content sync writes the manifest

apps/server/src/content/index.ts is generated: the contentTypes array ContentPlugin registers, plus a top-level export of every table drizzle-kit must diff — each type’s table, each many-relation’s join table, and the revisions table. Run content sync after adding, renaming or removing a file under collections/ or pages/, then generate:

npx orthacms content sync
# content sync: 3 type(s) → apps/server/src/content/index.ts
npx orthacms generate --name=add_recipe
  • The convention is the whole contract: collections/<name>.ts exports a collection named <name>, pages/<name>.ts a single. A file that breaks it stops the sync with the reason, rather than being guessed at.
  • An unchanged manifest is left alone and reported as already up to date.
  • It is the one command that loads TypeScript: type files import only @orthacms/content-server/define, which needs no compile step.
  • The schema builder renders the manifest with the same code, so a hand-run sync and an apply write identical files. In the monorepo the same command is npx nx run server:content:sync.

migrate compiles first

It builds the server, then reads the compiled config and plugin list — no TypeScript loader at runtime. Migrations run in plugin order, with no transaction across plugins. See migrations.

.env is loaded by the CLI

Nothing else loads it. Variables already exported are not overwritten, so a deployment’s environment wins:

DATABASE_URL=postgres://prod/… npx orthacms migrate   # overrides .env

The app's layout is a convention, not configuration

The CLI expects:

  • dist/server/src/main.js — start
  • dist/server/orthacms.config.js — migrate
  • dist/server/src/plugins.js — migrate

These follow from the generated tsconfig rootDirs. Change one and start reports a missing entry point.

Nothing is bundled

Each plugin finds its migrations relative to its own package in node_modules. A bundler would break that, so apps compile with tsc and keep node_modules on disk.

See installation and the database.