Two ways in. Scaffold an app that consumes the packages from npm, or clone the monorepo and work on Apograph itself.
Almost everybody wants the first.
Prerequisites
| Version | Why | |
|---|---|---|
| Node | 22 or newer | The CLI uses process.loadEnvFile and node --watch |
| PostgreSQL | 16 | The only supported database; the compose file and every deployment guide run 16 |
| npm | 10 or newer | Exact-pinned dependencies, no workspaces in a generated app |
A docker-compose.yml in the monorepo brings up a Postgres if you do not have
one.
Scaffolding an app
npx create-apograph-app my-cms
cd my-cmsThe wizard’s four questions are covered in the CLI
reference. Every one has a default, and --yes takes all
of them.
What you get
my-cms/
├── package.json one package — not workspaces
├── tsconfig.json references the four apps
└── apps/
├── server/ apograph.config.ts, src/{main,plugins}.ts
├── admin/ index.html, vite.config.mts, src/
├── server-e2e/ jest + supertest, against a real Postgres
└── admin-e2e/ Playwright, with /api mockedThe same four apps Apograph itself is built from, so anyone who has read the source finds the same shape in their project.
One package.json, deliberately
npm workspaces would let the server and admin halves resolve different copies
of a shared package — two @apograph/design-system instances means two React
contexts and a UI that silently stops talking to itself.
A scaffolded app ships a working test setup: Jest for the server (NestJS
needs emitDecoratorMetadata, which Vitest’s transform does not emit), Vitest
for the admin, and the two end-to-end suites. A starter that cannot be tested
teaches people not to.
Configure
cp .env.example .envDATABASE_URL is the only variable the app refuses to load without. Set
APOGRAPH_ROOT_ADMIN_EMAIL and APOGRAPH_ROOT_ADMIN_PASSWORD too, or you will have
a CMS with no way in.
Every other variable is optional and documented in configuration.
Migrate and boot
npx apograph migrate
npx apograph devmigrate builds the server and applies every plugin’s migrations in plugin
order. dev runs the API and the admin dev server together.
For a deployment, apograph build then apograph start — one process serves the API
and the built admin from one origin, which is what identity’s SameSite=lax
session cookie needs.
Verifying the install
The API reference is served on /reference outside production, generated from
the running app rather than written by hand. If your content type is in it, the
registry loaded and the routes are mounted.
Set API_DOCS=true to publish it from a deployed instance.
Upgrading
Every @apograph/* package is released in lockstep, pinned exactly, with
no caret.
npm install @apograph/content-server@0.5.2 @apograph/content-admin@0.5.2 # …and the rest
npx apograph migrateUpgrade the whole set, or none of it
A partial upgrade can leave two copies of a shared package in node_modules —
two React context instances, and an admin whose sidebar silently stops talking
to its provider.
Nothing errors. The UI simply stops working in a way that looks like a bug in your own code.
Run apograph migrate after every upgrade. A release that adds a table ships its
migration in the plugin that owns it.
Working on Apograph itself
git clone https://github.com/apograph-source/apograph-cms
cd apograph-cms
npm install
cp .env.example .env
docker compose up -d
npx nx run server:db:migrate
npm run devThe monorepo resolves every package from source, so a change in a package
is live in the app without a build step. Its Nx targets are thin adapters over
the same functions the apograph binary exposes — one implementation of “apply
migrations in plugin order”, because two would be two chances to get the most
destructive operation in the system wrong.
See project structure for the layout.