Running locally
This page is for contributors who want to run CairnCMS from the source tree, modify the code, and see their changes apply immediately. For running CairnCMS to use as a CMS, see Quickstart instead.
Prerequisites
Section titled “Prerequisites”- Node.js 22 LTS or newer. The platform targets Node 22 and runs CI against it.
- pnpm 10+. The repo pins
pnpm@10.13.1as itspackageManager. Use corepack (corepack enable) to pick up the pinned version automatically. - Docker and Docker Compose, for the database and supporting services. The root
docker-compose.ymlprovides every supported database vendor on local ports. - Git, with a configured user and email. Commits must be signed off (
git commit -s) per the Developer Certificate of Origin.
A few hundred MB of free RAM and disk space are also needed during dependency install and build.
Clone and install
Section titled “Clone and install”git clone https://github.com/CairnCMS/cairncms.gitcd cairncmspnpm installpnpm install walks the workspace and links every internal package against every other. The first install takes a few minutes.
After install, build the workspace once:
pnpm buildThis compiles every workspace member except docs/ in dependency order. Subsequent dev sessions only build the package you are actively working on (more on that below).
Choose a database
Section titled “Choose a database”The platform supports SQLite, PostgreSQL, MySQL, MariaDB, and others. For local development, two paths:
SQLite (simplest)
Section titled “SQLite (simplest)”No services to start. Set DB_CLIENT=sqlite3 in your .env (covered below) and point DB_FILENAME at a local file. Good for quick iteration, exploring the platform, and running unit tests.
A server vendor through Docker Compose
Section titled “A server vendor through Docker Compose”The root docker-compose.yml brings up every supported vendor on local ports. Start whichever you need:
# Postgres on port 5100docker compose up -d postgres
# MySQL 8 on port 5101docker compose up -d mysql
# MariaDB on port 5102docker compose up -d mariaThe compose file’s header comment lists the full port and credential matrix. The exposed ports are in the 5xxx range so they don’t collide with the blackbox compose stack (6xxx).
For a setup that mirrors what most operators run in production, Postgres is the right choice. The local credentials are postgres / secret against the cairncms database on port 5100.
Configure the environment
Section titled “Configure the environment”Create api/.env with at minimum:
KEY=local-dev-keySECRET=local-dev-secret
DB_CLIENT=pgDB_HOST=localhostDB_PORT=5100DB_DATABASE=cairncmsDB_USER=postgresDB_PASSWORD=secret
PUBLIC_URL=http://localhost:8055
ADMIN_EMAIL=admin@example.comADMIN_PASSWORD=passwordFor SQLite, replace the DB_* block with:
DB_CLIENT=sqlite3DB_FILENAME=./data.dbRelative paths in api/.env resolve against the API package’s working directory, so ./data.db lives at api/data.db when you run the dev server from the repo root with pnpm --filter api run dev. Use an absolute path if you want the file somewhere else.
KEY and SECRET can be any strings for local development; in production they need to be cryptographically random. ADMIN_EMAIL and ADMIN_PASSWORD create the initial admin account on first bootstrap.
The full configuration reference, including auth providers, mail, storage, cache, and the rest, is in Configuration.
Bootstrap and start
Section titled “Bootstrap and start”The API has a two-step lifecycle: bootstrap once to set up system tables, then run the dev server.
# From the repo root, run the API's CLI bootstrap oncepnpm --filter api run cli bootstrap
# Then start the dev server with hot reloadpnpm --filter api run devpnpm --filter api run dev runs tsx watch on src/start.ts, which restarts the server whenever you save a TypeScript file in api/src/. The server defaults to port 8055.
The bootstrap step:
- Creates every
directus_*system table. - Applies all migrations.
- Creates the admin user from
ADMIN_EMAILandADMIN_PASSWORD(only on first run).
Re-run bootstrap whenever you pull changes that add new migrations.
Run the admin app
Section titled “Run the admin app”In a second terminal, start the Vue app:
pnpm --filter app run devThis runs Vite on port 8080 by default and proxies API calls to http://localhost:8055. Open http://localhost:8080/admin/ in a browser; the app loads with hot module replacement.
For most contributing work, both the api dev server and the app dev server need to be running. Keep them in two terminal panes.
Three test layers run independently:
-
Unit tests (per-package, fast):
Terminal window pnpm testRecursively runs
vitest --watch=falsein every workspace member except the blackbox suite. Most contributions need this to pass before review. -
Blackbox tests (slow, requires Docker):
Terminal window # Start the supporting servicesdocker compose -f tests/blackbox/docker-compose.yml up auth-saml redis minio minio-mc -d# Rebuild before running the suite (see note below)pnpm build# For SQLite (the cheapest path; what PR CI runs)TEST_DB=sqlite3 pnpm test:blackbox# For other vendors (start the matching DB container first)docker compose -f tests/blackbox/docker-compose.yml up postgres -dTEST_DB=postgres pnpm test:blackboxpnpm test:blackboxdeploys whatever is already in each package’sdist/directory rather than rebuilding from source. After any source change inapi/,packages/, orsdk/, runpnpm build(or rebuild the affected package withpnpm --filter <name> run build) before invoking the suite, or the tests run against stale compiled output.The blackbox suite is the highest-coverage layer. Run at least the SQLite path before submitting; run the relevant server vendor when changes affect SQL generation. See Repository layout /
tests/for the full vendor matrix. -
Lint:
Terminal window pnpm lintRuns ESLint across the workspace. Auto-fixable issues can be addressed with
pnpm lint --fix.
Common workflows
Section titled “Common workflows”Working on the API
Section titled “Working on the API”Edit files in api/src/. The dev server restarts automatically on save. Most controllers, services, and middleware are reloadable; database migrations require a re-bootstrap if you add a new migration file under api/src/database/migrations/.
Working on the admin app
Section titled “Working on the admin app”Edit files in app/src/. Vite reloads modules in the browser on save. Vue components hot-replace; route or store changes occasionally need a full page reload.
Working on a shared package
Section titled “Working on a shared package”Packages under packages/ are imported by api, app, and sdk through pnpm’s symlinking, so source changes are visible immediately. Some packages need a build step to publish their compiled output:
# Rebuild a single package after editing itpnpm --filter @cairncms/utils run build
# Or run its build in watch modepnpm --filter @cairncms/utils run devThe api and app dev servers pick up the rebuilt output on their next reload.
Working on the SDK
Section titled “Working on the SDK”pnpm --filter @cairncms/sdk run devWatches sdk/src/ and rebuilds on change. Local consumers (other workspace members or external projects linked with pnpm link) see the new build immediately.
Resetting state
Section titled “Resetting state”Local databases accumulate state quickly during development. To start fresh:
# SQLite (the file lives in api/, per the relative path in api/.env)rm api/data.dbpnpm --filter api run cli bootstrap
# Postgres / MySQL / MariaDB through composedocker compose down -vdocker compose up -d postgrespnpm --filter api run cli bootstrapdocker compose down -v removes the named volumes that hold database state. The up -d brings the service back up empty.
Troubleshooting
Section titled “Troubleshooting”pnpm installfails on Node 24+ — the platform targets Node 22 LTS. If you must run on a newer version, expect occasional dependency-version mismatches; the lockfile is pinned against Node 22.- The api dev server fails to start with a database error — the most common cause is the database not being up yet.
docker compose psshows the running services;docker compose logs <service>shows why something failed. - The admin app loads but says “API not reachable” — check that the api dev server is running on
8055and thatPUBLIC_URLinapi/.envmatches the URL the app is making requests against. - A blackbox test fails on first run — check that the supporting services are healthy with
docker compose -f tests/blackbox/docker-compose.yml ps. The first run after a Docker restart sometimes hits a service-still-starting race; rerun once everything ishealthy. - Changes in a
packages/library don’t show up in the api or app — the package may need a manual rebuild. Runpnpm --filter <package> run buildand reload the dev server.
Where to go next
Section titled “Where to go next”- Repository layout — the map of workspace members and where each piece of functionality lives.
- Contributing overview — branching, the canonical
CONTRIBUTING.mdandAI_POLICY.mdfiles, and review expectations. - Configuration — every environment variable that the API reads.
- Extensions — building extensions, which is a different setup story (a separate package with its own dependencies).