Skip to content

Schema as code

CairnCMS treats your data model as state that can be captured to a file, reviewed as a diff, and applied to another environment. The same file format works for environment promotion (dev to staging to production), peer review in pull requests, and a structural baseline you can replay against an empty database.

The two CLI commands cairncms schema snapshot and cairncms schema apply are the primary surface. An HTTP API equivalent exists for CI/CD pipelines that prefer not to shell into the container.

This page covers what’s in a snapshot, the CLI workflow, the HTTP equivalent, and the cross-environment caveats that decide whether a snapshot is portable.

A schema snapshot contains:

  • Collections — the user-defined collections in the database, with their metadata (name, icon, visibility, sort behavior, archive configuration, and so on).
  • Fields — every field on every user-defined collection, with type, defaults, validation, conditional logic, interface and display configuration, options, special behaviors, and translations.
  • Relations — the foreign-key and alias relations between collections, including junction tables for many-to-many and many-to-any setups.

It does not contain:

  • Content. Items in your collections, file rows in directus_files, or any application data.
  • Files on disk or in a remote storage backend.
  • Users, roles, and permissions. These are handled by config-as-code which is a separate workflow with its own command.
  • System tables and system fields. Anything CairnCMS owns is filtered out. The snapshot describes the project schema, not the platform.

Use schema-as-code for the structural side of the model. Use database backups (covered in Backups) for content.

The file’s top-level shape:

version: 1
directus: 1.0.0
vendor: postgres
collections: [...]
fields: [...]
relations: [...]

version is the snapshot format version; CairnCMS currently emits 1. directus is the platform version that produced the snapshot. vendor is the normalized database vendor name — one of postgres, mysql, sqlite, cockroachdb, oracle, mssql, or redshift. These three fields turn into a portability check at apply time. See Cross-environment caveats below.

The bulk of the file is the three sorted arrays of collections, fields, and relations. Sorting is deterministic so snapshots produced from equivalent schemas diff cleanly in source control.

Write the current schema to a YAML file:

Terminal window
cairncms schema snapshot ./schema.yaml

The CLI is interactive by default. If the target file already exists, it asks before overwriting. To run unattended (CI, scripted deploys), pass --yes:

Terminal window
cairncms schema snapshot --yes ./schema.yaml

YAML is the default output format. To emit JSON instead:

Terminal window
cairncms schema snapshot --format json ./schema.json

Omit the path to write to stdout, which is convenient for piping into other tools or generating filename patterns:

Terminal window
cairncms schema snapshot --yes > "./snapshots/$(date +%F).yaml"

YAML is the right default for files committed to source control: human-readable, diffs cleanly, comments allowed in the format. JSON is useful when a downstream tool requires it.

Apply a snapshot file to the current database:

Terminal window
cairncms schema apply ./schema.yaml

The command:

  1. Loads the snapshot file (YAML or JSON, detected from the extension).
  2. Reads the current schema from the database and computes a structural diff against the snapshot.
  3. If the diff is empty, exits with No changes to apply.
  4. If the diff has changes, prints them grouped by collections, fields, and relations, with creates in green, updates in blue, and deletes in red.
  5. Prompts for confirmation before applying. Apply only proceeds on y.

Two flags adjust the flow:

  • --dry-run — print the planned changes and exit without applying. Useful for CI checks and pre-deploy review.
  • --yes — skip the confirmation prompt and apply non-interactively.

The two are mutually exclusive in practice: --dry-run always exits without applying, and --yes only suppresses the prompt around an apply.

Applying a snapshot can drop columns when fields are removed and drop tables when collections are removed. The platform does not preserve content from a dropped column. Treat --dry-run as a required step before running --yes against a non-disposable database.

The intended pattern for a multi-environment project:

  1. Develop the schema interactively in your dev instance (the app builder is the easiest editing surface).
  2. Run cairncms schema snapshot ./schema.yaml to write the result to a file in your repo.
  3. Commit the snapshot. The diff in the pull request shows the schema change. Reviewers see “added field posts.published_at: timestamp”.
  4. Once merged, your deploy pipeline runs cairncms schema apply --yes ./schema.yaml against staging, then production, after the platform upgrade step.

The snapshot is the source of truth that travels with the code. Direct schema edits to a non-dev environment will be detected as drift the next time apply runs against that environment. apply reconciles the database to the snapshot, not the other way around.

For larger teams, prefer running cairncms schema apply --dry-run in CI on every PR that touches the snapshot file. The diff that prints in the CI log is the same diff the deploy will apply, so reviewers get a preview without anyone needing to run the deploy locally.

For pipelines that cannot shell into a container, the same workflow is exposed as three HTTP endpoints. All require an admin token.

  • GET /schema/snapshot — returns the current snapshot as JSON. Equivalent to cairncms schema snapshot --format json to stdout.
  • POST /schema/diff — accepts a snapshot (JSON in the body, or YAML/JSON as multipart form upload), returns { hash, diff }. The hash is a fingerprint of the current database snapshot at the moment the diff was computed.
  • POST /schema/apply — accepts the { hash, diff } payload from the diff endpoint (JSON in the body, or YAML/JSON as multipart form upload) and applies the diff. The hash is re-checked against the current state. If the database has changed since the diff was produced, the apply is rejected.

The two-step diff/apply flow is the safety net for HTTP. Between the moment you compute the diff and the moment you apply it, another admin or an automated process might have changed the schema. The hash check catches that and forces a re-diff.

The HTTP /schema/diff endpoint also validates that the snapshot’s directus version and vendor fields match the running instance. The check can be bypassed by passing ?force on the request, but the CLI does not enforce this validation at all.

The fields stamped into the snapshot (directus, vendor) describe the environment the snapshot was produced from. Carrying a snapshot to an environment that differs along either axis is a known sharp edge:

  • Different platform version. The HTTP /schema/diff endpoint refuses snapshots from a different platform version unless force is set. The CLI does not check; it will run the diff and apply against whatever version is running. Treat platform-version mismatches as a hint to upgrade or downgrade first, not a routine --force flag.
  • Different database vendor. The same restriction. Some snapshots happen to apply cleanly across Postgres, MySQL, and SQLite; others do not, because column types, default expressions, and index behavior differ at the SQL level. Snapshot portability across vendors is not a guaranteed property of the format.
  • Drift between dev and prod. A snapshot produced from a dev instance that has been edited interactively in prod will diff against the prod state. Decide before applying whether the prod-side edits should be preserved (re-snapshot from prod and merge) or overwritten (apply the dev snapshot as-is).

The conservative posture: keep environments aligned on platform version and database vendor, and let apply reconcile structural drift in one direction (file → database).

Two adjacent surfaces look similar from a distance and need their own workflows:

  • Roles and permissions. Captured by cairncms config snapshot and cairncms config apply — see Config as code. The two commands are deliberately separate because the lifecycle of a permission change differs from the lifecycle of a schema change.
  • Content and seed data. Schema-as-code never captures application data. For seeding a fresh environment with example content, use a database dump and restore (see Backups) or write a custom migration that inserts the seed rows (Custom migrations).

Schema-as-code is the structural baseline. Layer config-as-code on top for permissions, and a content-loading mechanism on top of that for data. Each surface has its own diffing semantics.

  • Config as code — the same diff/apply pattern for roles and permissions.
  • Migration between instances — covers moving a complete deployment, of which a schema snapshot is one piece.
  • Custom migrations — the procedural alternative when a change is too imperative for a snapshot to capture cleanly (data backfills, complex transforms, conditional logic).