# Deploying schema changes
> Diff and push code-owned schemas, understand the safe/destructive classification, and roll back with immutable versions.
Schema deployment is a two-step CLI workflow: `myna schema diff` shows a stable, classified plan against the deployed state, and `myna schema push` applies it, creating immutable schema versions. Nothing in the dashboard mutates schemas — changes only flow from your [schema files](/schema/defining-schemas) through the CLI.
```bash
$ myna schema diff
+ add_field posts.subtitle — Add field "subtitle".
~ modify_field posts.title — Field "title" becomes required.
! remove_field posts.legacy — Remove field "legacy".
classification: destructive
$ myna schema push --allow-destructive --summary "Drop legacy field"
```
## How local schemas are loaded
Both commands read a schema directory — `myna/` by default, resolved relative to the linked project root, or an explicit `--schema-dir
`. Every `.ts`, `.mts`, `.js`, or `.mjs` file in the directory (recursively, skipping dotfiles, `node_modules`, and `dist`; `.d.ts` files are ignored) is compiled and inspected. Each file may:
- default-export a single `collection(...)`,
- export an array of collections, or
- export several named collections.
Files are bundled in memory with esbuild and imported; any export (or array element) that looks like a collection is collected, deduplicated by `name` (last definition wins), canonicalized, and sorted by name. Imports of `@myna-sh/sdk/schema` (and the internal `@myna-sh/schema`) are aliased to the DSL copy bundled with the CLI itself, so schema loading works without the SDK installed and canonicalization always matches the CLI's version.
## Change classification
The server diffs the proposed schemas against the latest deployed version of each collection and classifies every operation. The plan is deterministic: collections sorted by key, fields by key within each. The diff's overall classification is the highest severity across its operations.
| Class | Marker | Meaning |
|---|---|---|
| `safe` | `+` | Cannot invalidate or lose existing content. |
| `conditionally_destructive` | `~` | Valid only if existing content already satisfies the new constraints; push validates content and fails otherwise. |
| `destructive` | `!` | Removes or reshapes data. Requires `--allow-destructive`. |
### Safe
- Adding a collection.
- Adding an optional field, or a required field that has a default.
- Collection metadata changes: `label`, `titleField`, `path`, slug config.
- Making a collection `public`.
- Cosmetic field changes: `label`, `description`, `ui`, `default`.
- Making a field optional, or loosening constraints (raising `maxLength`, widening `min`/`max`, growing an `enum`, allowing `multiple`).
### Conditionally destructive
- Adding a required field without a default — fails when entries exist (see below).
- Making an existing field required.
- Making a collection `private` (published consumers lose access).
- Tightening constraints: lowering `maxLength`/`max`/`maxItems`, raising `minLength`/`min`/`minItems`, adding or changing a text `pattern`, adding or shrinking an `enum`, adding `integer` to a number, or turning `multiple: true` into `multiple: false` on an asset or reference field.
### Destructive
- Removing a collection or removing a field (this includes renaming a key — the diff sees a remove plus an add).
- Changing a field's type.
- Changing a collection's `kind` (collection ↔ singleton).
- Retargeting a reference field to a different collection.
- Changing a list's item kind.
## --allow-destructive
`myna schema push` prints the plan first. If any operation is destructive and `--allow-destructive` was not passed, the push aborts client-side with an error. The flag is also enforced server-side: the API rejects destructive pushes without it (`SCHEMA_CHANGE_DESTRUCTIVE`), which the CLI reports as the same instruction to re-run with the flag. Conditionally destructive operations do not require the flag — they succeed or fail on content validation instead.
## Content validation on push
Before applying anything, the server validates the current draft and published head revision of every existing entry against the proposed schemas. Any incompatibility — most commonly adding a required field without a default to a collection that already has entries — fails the whole push with per-entry validation errors, and nothing is applied. `myna schema diff` reports the same errors ahead of time as `content error:` lines, so you can catch them before pushing. To add a required field to a populated collection, either supply a `default` or backfill the field in existing entries first (via a [change set](/concepts/change-sets)) and then push.
## Immutable versions and rollback
Every push that changes a collection creates a new immutable `collection_versions` row with an incrementing version number, the canonical schema JSON, a content hash, the actor, and the optional `--summary` text. Existing versions are never edited. All changes in a push are applied in a single transaction.
Rollback is just another push: check out (or `git revert` to) the earlier schema files and run `myna schema push` again. If a collection's proposed schema hashes identically to a previously deployed version, the server points the collection back at that existing version instead of minting a duplicate — the version history stays linear and complete. Pushing files identical to the deployed state is a no-op (editable metadata like the label is still synced).
## myna schema pull
`myna schema pull` materializes the deployed schemas as local DSL files — one `.ts` per collection, written into the schema directory (`--schema-dir` to override):
```bash
$ myna schema pull
wrote /path/to/repo/myna/posts.ts
wrote /path/to/repo/myna/authors.ts
```
Use it to bootstrap a repository against an existing project, or to reconcile drift before a diff. `myna schema inspect [collection]` lists deployed collections or one collection's fields without touching the filesystem.
### Pulling drift into a pull request
Reporting drift and doing nothing about it is how a repository stops being the source of truth. `--open-pr` closes the loop: it commits the deployed schema onto its own branch and opens a pull request, so a schema someone changed elsewhere arrives as a reviewable change rather than a warning.
```bash
$ myna schema pull --open-pr
Opened https://github.com/acme/site/pull/214 against main.
```
What it does, in order: renders the deployed schemas, keeps only the files that actually differ, refuses to continue if anything under the schema directory is already modified (that work would otherwise be swept into the commit), writes the changed files, commits them on a new branch, pushes, and opens the pull request through the [GitHub CLI](https://cli.github.com) — which is where your credential and remote already live. The working tree is then put back on the branch you started from, so your checkout does not silently acquire a schema you have not read.
- Nothing to do when the files already match — it says so and exits 0.
- The branch name is derived from the content (`myna/schema-`), so re-running on unchanged drift finds the branch already there and does not open a second pull request. Override with `--branch`, and `--base` to target something other than the current branch.
- The description lists the files and the operations a `schema push` *would* have applied before the change — the drift itself, stated in the direction a reviewer thinks in.
`myna doctor`'s `schema.drift` check names this as the remedy when the deployed schema is the one to keep; `myna schema push` remains the remedy when the repository is.
## Environments and promotion
Myna models environments as projects: keep separate `my-site-dev`, `my-site-staging`, and `my-site` projects, each with its own API keys, origins, webhooks, and preview templates. Two operations keep their schemas aligned:
- **Drift detection** — `GET /v1/projects/:project/schema/drift?against=` (or `myna schema drift --against my-site-staging`) diffs the two projects' *deployed* schemas and reports the ops, classification, and `inSync`. The CLI exits non-zero on drift, so it slots into CI.
- **Promotion** — `POST /v1/projects/:project/schema/promote` with `{"fromProject": "...", "confirm": true}` (or `myna schema promote --from my-site-staging`) pushes the source project's deployed schemas into the target through the exact same pipeline as `schema push`: existing content is validated against the incoming schemas, promotions that would invalidate entries are rejected, and destructive changes require `allowDestructive`. Each promotion records immutable versions and an audit event.
For content, clone between environments with [`myna projects export`](/management-api/overview) and `myna entries import` — imports stage on a change set, so promoted content still goes through validate → preview → publish.
## Next
- [Generated TypeScript types](/schema/generated-types) — regenerate types after every push.
- [Quickstart](/getting-started/quickstart) — the full end-to-end flow.