Ortha is an Nx monorepo on npm workspaces (not pnpm, not yarn).
apps/
admin/ React 19 + Vite SPA
server/ NestJS API — and your content types
admin-e2e/ Playwright suite for the admin
server-e2e/ Testcontainer + supertest suite for the API
packages/
bootstrap/{admin,server} the two hosts
database/ the connection, the outbox, the unit of work
identity/{domain,server,admin,provider-*} auth, RBAC, SSO
workspaces/{server,admin} workspaces, grants, sharing
content/{domain,server,admin,graphql}
schema-builder/{domain,server,admin} the Content Model page
media/{domain,server,admin,provider-*}
mail/{domain,server,provider-*}
copilot/{domain,server,admin,provider-*}
tools/server the agent tool registry the copilot and MCP share
mcp/server the Model Context Protocol endpoint
utils/server the environment readers (readEnv, readFlag, …)
cli/ the `orthacms` binary
create-orthacms-app/ the scaffolder
…This is the monorepo, not a generated app
An app from npx create-orthacms-app has the same four apps/ and no packages/ — it installs them from npm. See installation.
How packages are laid out
- Flat —
packages/design-system— or grouped by domain with a runtime split —packages/content/server,packages/content/admin. - The npm name is hyphenated regardless of nesting:
packages/bootstrap/admin→@orthacms/bootstrap-admin. - A group holds what the domain needs: a framework-free
domainkernel, extra protocols (graphql), andprovider-*adapters for swappable backends.
Where your code goes
For most projects: apps/server, nothing else.
| Path | What you put there |
|---|---|
apps/server/src/content/collections/ | One file per collection |
apps/server/src/content/pages/ | One file per single |
apps/server/src/content/index.ts | The generated content manifest — rewritten by content:sync or the schema builder, never by hand |
apps/server/src/plugins.ts | Which plugins run, and in what order |
apps/server/config/*.ts | One module per plugin — the only code that reads the environment |
apps/server/orthacms.config.ts | Assembles the config/ modules into one typed object |
apps/server/migrations/ | Generated SQL for your content tables — committed |
Adding a capability rather than content? Write a plugin — see extending Ortha.
Commands
npx nx <typecheck|build|lint|test|serve> <project>
npx nx sync # after changing cross-project dependencies
npx nx graph # visualise the project graphRunning the stack
npm run dev # the whole stack, four Nx panes
npm run start:server # API only
npm run start:admin # admin only| Pane | What it is |
|---|---|
server:dev:build | webpack in watch mode — compile and type errors land here |
server:dev:run | node --watch on the built entry; Nest logs only |
admin:dev | Vite dev server, proxying /api to the API |
admin:dev:typecheck | tsc --build --watch — Vite never typechecks |
Database
docker compose up -d # Postgres 16
docker compose --profile sso up -d # + Keycloak on :8080, for SSO work
npx nx run server:db:migrate # apply every plugin's migrations
npx nx run <plugin>:db:generate --name=<change> # generate one plugin's migration
npx nx run server:db:studio # browse the live database
npx nx run server:content:sync # regenerate src/content/index.tsThese nx run targets exist only in the monorepo (inferred by the private @orthacms/nx plugin). A generated app runs the same operations as npx orthacms migrate, generate, studio and content sync — see the CLI.
The type files and the manifest can also be written by the schema builder: SCHEMA_BUILDER=true npm run dev, then Content Model in the admin.
Running several stacks at once
A slot fixes a worktree’s ports and database: slot n = API :300n, admin :420n, database orthacms_an.
npm run worktree -- provision <slot> --path <worktree>
npm run worktree -- list
npm run worktree -- release <slot> --yesSlots share one Postgres container. Two concurrent stacks is the realistic ceiling on a four-core machine.
Conventions
- Prettier, four-space indent, single quotes.
- Admin i18n is
react-intl; each component co-locates its owndefineMessagesblock. - House style is encoded as skills in
.agents/skills/:server-plugin,admin-plugin,accessibility,admin-e2e,server-e2e,shadcn(plus workflow skillsticket,open-pr,agent-browser). - Every directory with project context has an
AGENTS.mdand aCLAUDE.mdthat imports it. EditAGENTS.md.
The API reference
http://localhost:3000/reference # Scalar UI
http://localhost:3000/reference/json # raw OpenAPI documentOn outside production. API_DOCS=true publishes it from a deployment; API_DOCS=false turns it off locally.