Two ways in:
- Scaffold an app that consumes
@orthacms/*from npm. Almost everybody wants this. - Clone the monorepo to work on Ortha itself.
Prerequisites
| Version | Why | |
|---|---|---|
| Node | 22 or newer | The CLI uses process.loadEnvFile and node --watch |
| PostgreSQL | 16 or newer | The only supported database. Tests run on 16; the generated compose file starts 17 |
| npm | 10 or newer | Exact-pinned dependencies, no workspaces in a generated app. create-orthacms-app 0.7.0 installs cleanly on npm 10 |
Scaffold an app
npx create-orthacms-app my-cms
cd my-cmsThe wizard asks for an app name, a database URL and an admin email, then five choices. Each has a flag, so it can run unattended:
| Flag | Choices | Default |
|---|---|---|
--media <id> | media-local, media-s3, media-azure, media-gcs, media-vercel-blob | media-local |
--copilot <ids> | copilot-anthropic, copilot-openai, or none | none |
--sso <ids> | sso-oidc, sso-github, sso-saml, or none | none |
--protocols <ids> | graphql, mcp, or none (REST is always on) | none |
--mail <id> | mail-smtp, or none | none |
--yes | Accept every default, ask nothing | — |
--no-install / --no-git | Skip npm install / git init | — |
# Unattended: S3 media, GraphQL + MCP, SMTP mail
npx create-orthacms-app my-cms --yes \
--media media-s3 --protocols graphql,mcp --mail mail-smtpFull reference: the CLI.
What you get
my-cms/
├── package.json one package — not npm workspaces
├── docker-compose.yml Postgres for local development
├── .env written by the scaffolder
├── tsconfig.json references the four apps
└── apps/
├── server/ orthacms.config.ts, config/*.ts, src/{main,plugins}.ts,
│ src/content/ (your types), migrations/, drizzle.config.ts
├── admin/ index.html, vite.config.mts, src/{main,plugins}.ts
├── server-e2e/ Jest + supertest, against a real Postgres
└── admin-e2e/ Playwright, with /api mocked- One
package.json, on purpose. Workspaces could resolve two copies of@orthacms/design-system— two React contexts, and a UI that silently stops working. - Tests work out of the box.
npm testruns the unit tests (Jest for the server, Vitest for the admin);npm run e2eruns both end-to-end suites. - No content types yet — but their plumbing is there: the generated manifest, the drizzle config and a baseline migration. Add types on the admin’s Content Model page (the schema builder, with
SCHEMA_BUILDER=true) or by hand; the quick start shows both.
Configure
The scaffolder writes .env:
DATABASE_URL=postgresql://orthacms:orthacms@localhost:5432/my_cms
ORTHACMS_ROOT_ADMIN_EMAIL=admin@example.com
ORTHACMS_ROOT_ADMIN_PASSWORD=… # generated, printed onceDATABASE_URLis the only variable a default app refuses to boot without.- The root admin is provisioned on boot, idempotently. Keep both
ORTHACMS_ROOT_ADMIN_*values set until you have signed in. - With mail chosen,
MAIL_PROVIDER=smtpalso requiresAPP_URL,MAIL_FROMandSMTP_HOST.
Everything else is optional — see configuration.
Migrate and boot
docker compose up -d # if you have no Postgres
npm run migrate # orthacms migrate
npm run dev # orthacms dev — API :3000, admin :4200| npm script | Runs | Does |
|---|---|---|
npm run dev | orthacms dev | tsc --watch + node --watch + Vite |
npm run migrate | orthacms migrate | Build, then apply every plugin’s migrations in plugin order |
npm run generate | orthacms generate | Generate SQL for your content tables |
npm run content:sync | orthacms content sync | Rewrite the content manifest, src/content/index.ts |
npm run build | orthacms build | Server into dist/server, admin into dist/admin |
npm start | orthacms start | One process serving the API and the built admin from one origin |
npm run studio | orthacms studio | Drizzle Studio on the live database |
Verify the install
curl -s http://localhost:3000/reference/json | head -c 200 # the raw OpenAPI document
# the interactive Scalar UI is at http://localhost:3000/referenceThe reference is generated from the running app and served outside production. Your content types appear in it once registered. Set API_DOCS=true to publish it from a deployment.
Upgrading
Every @orthacms/* package is released in lockstep and pinned exactly. Move them all at once:
npm install @orthacms/content-server@0.7.0 @orthacms/content-admin@0.7.0 # …and every other @orthacms/* package
npx orthacms migrate
npm ls @orthacms/design-system react # must show exactly one of eachUpgrade the whole set, or none of it
A partial upgrade can leave two copies of a shared package in node_modules — two React contexts, and an admin that silently stops working. Nothing errors.
The one-command bump and per-release notes are on upgrading.
Working on Ortha itself
git clone https://github.com/ortha-source/ortha-cms
cd ortha-cms
npm install
cp .env.example .env
docker compose up -d
npx nx run server:db:migrate
npm run dev- Packages resolve from source — a change in a package is live with no build step.
- The Nx targets call the same functions the
orthacmsbinary does.
See project structure.