+
+
+
+
diff --git a/content/guides/10.environment-sync/.navigation.yml b/content/guides/10.environment-sync/.navigation.yml
new file mode 100644
index 00000000..b8061f24
--- /dev/null
+++ b/content/guides/10.environment-sync/.navigation.yml
@@ -0,0 +1 @@
+title: Environment Sync
diff --git a/content/guides/10.environment-sync/0.index.md b/content/guides/10.environment-sync/0.index.md
new file mode 100644
index 00000000..158fcd2a
--- /dev/null
+++ b/content/guides/10.environment-sync/0.index.md
@@ -0,0 +1,67 @@
+---
+stableId: 393b999d-c4ed-4b83-9208-cf088a4918ab
+title: Overview
+description: Move schema and configuration between Directus instances through sync files you commit to git, using the Directus CLI.
+---
+
+You build your data model on a development instance, and at some point that work has to reach staging and production. Environment Sync makes that a git workflow: the Directus CLI (`directus-cli`, or its short alias `d6s`) writes an instance's **Schema and Configuration** to JSON sync files you commit, then applies those files to any instance you point it at.
+
+```bash
+d6s sync pull --from staging # write the instance's Schema + Configuration to sync files
+d6s sync diff --to production # preview what pushing the sync files would change
+d6s sync push --to production # apply them
+```
+
+Because the sync files live in your repository, you can review changes in pull requests, promote them through git history, and use a revert plus another push to restore an earlier state. `d6s sync` on its own runs a small interactive wizard that pulls and pushes in one pass.
+
+The CLI is built to be safe to point at production: `diff` never applies anything, deletions happen only in `mirror` mode behind their own explicit consent (that includes removals during a rollback), and the CLI asks you to resolve records that match more than one target record. It never guesses. [How It Works](/guides/environment-sync/how-it-works) covers the full safety model.
+
+## What syncs
+
+- **Schema**: every collection, field, and relation, including custom fields on system collections.
+- **Configuration**: roles, policies, access, permissions, flows, operations, dashboards, panels, settings, media-library folders, and custom Data Studio translation strings.
+- **Opt-in**: user accounts, with every secret field stripped.
+
+Unlike a full schema snapshot, a pull is not all-or-nothing. `--collections posts` narrows which Schema files it overwrites, and resource flags like `--flows` or `--no-schema` choose which Configuration files it touches. That is how you promote finished work from a shared development instance while unfinished work stays out of the sync files entirely. Scope is by collection and resource type, not by individual record. See [Promote only the changes that are ready](/guides/environment-sync/common-workflows#promote-only-the-changes-that-are-ready).
+
+Records in your own collections are **content**, and Environment Sync does not sync them. It moves the shape of a project and its configuration, not its content.
+
+::callout{icon="i-lucide-info"}
+Content sync is deferred to a future release. Cross-instance record identity for integer primary keys, file references, and user references needs its own architecture, and a wrong guess there could overwrite content on the target.
+::
+
+## Before you start
+
+- **Both instances must run Directus 12.2.0 or later, at the same exact version and on the same database vendor.** The patch release must match. The server refuses an incompatible schema comparison: some patches change the snapshot format, and database vendors describe column types differently. `--allow-drift` bypasses the check when you accept that risk; see [the compatibility gate](/guides/environment-sync/how-it-works#the-compatibility-gate).
+- **An admin credential for each instance.** A static token from an admin user is the usual choice. The CLI verifies this and refuses non-admin tokens: the server rejects non-admin schema and import writes, and non-admin reads are silently filtered by permissions, which would produce sync files that look complete but aren't.
+- **A git repository.** The sync files are designed for review and versioning. Any repository works; many teams use the one that already holds their Directus deployment configuration.
+
+## Where to go
+
+::card-group
+
+:::card{title="Quickstart" icon="i-lucide-rocket" to="/guides/environment-sync/quickstart"}
+Run the full pull, diff, push loop against two throwaway instances and see every command's output.
+:::
+
+:::card{title="How It Works" icon="i-lucide-lightbulb" to="/guides/environment-sync/how-it-works"}
+The mental model: sync files as the source of truth, record identity, push phases, and the safety rules.
+:::
+
+:::card{title="Common Workflows" icon="i-lucide-map" to="/guides/environment-sync/common-workflows"}
+Promote changes, ship only what's ready, adopt sync on an existing project, roll back, recover from drift.
+:::
+
+:::card{title="CI & Automation" icon="i-lucide-bot" to="/guides/environment-sync/ci-and-automation"}
+Post the production diff on pull requests and push on merge, with tokens and JSON reports.
+:::
+
+:::card{title="Secrets & Limitations" icon="i-lucide-shield-check" to="/guides/environment-sync/secrets-and-limitations"}
+How secret values are kept out of your repository, and what Environment Sync deliberately does not do.
+:::
+
+:::card{title="Reference" icon="i-lucide-list-checks" to="/guides/environment-sync/reference"}
+Every command, flag, table, and report format in one place.
+:::
+
+::
diff --git a/content/guides/10.environment-sync/1.quickstart.md b/content/guides/10.environment-sync/1.quickstart.md
new file mode 100644
index 00000000..49f6cc09
--- /dev/null
+++ b/content/guides/10.environment-sync/1.quickstart.md
@@ -0,0 +1,248 @@
+---
+stableId: 04fa4fa3-2151-44c3-ba63-9433d180cbec
+title: Quickstart
+description: Run the full pull, diff, push loop against two throwaway Directus instances, and see what the CLI does at every step.
+---
+
+The fastest way to trust Environment Sync is to watch it work somewhere mistakes are free. In this guide, you stand up two throwaway Directus instances with Docker, make a change on one, and move it to the other through git. Nothing here touches a real project, and one command tears it all down at the end.
+
+You need Docker, Node.js 22 or later with npm, and git.
+
+## Start two instances
+
+Save this as `docker-compose.yml` in an empty directory:
+
+```yaml
+services:
+ source-db:
+ image: postgres:16-alpine
+ environment:
+ POSTGRES_USER: "directus"
+ POSTGRES_PASSWORD: "directus"
+ POSTGRES_DB: "directus"
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U directus"]
+ interval: 5s
+ retries: 12
+
+ source:
+ image: directus/directus:12.2.0
+ ports:
+ - 8055:8055
+ environment:
+ SECRET: "quickstart-source-secret"
+ DB_CLIENT: "pg"
+ DB_HOST: "source-db"
+ DB_PORT: "5432"
+ DB_DATABASE: "directus"
+ DB_USER: "directus"
+ DB_PASSWORD: "directus"
+ ADMIN_EMAIL: "admin@example.com"
+ ADMIN_PASSWORD: "quickstart"
+ ADMIN_TOKEN: "source-token"
+ depends_on:
+ source-db:
+ condition: service_healthy
+
+ target-db:
+ image: postgres:16-alpine
+ environment:
+ POSTGRES_USER: "directus"
+ POSTGRES_PASSWORD: "directus"
+ POSTGRES_DB: "directus"
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U directus"]
+ interval: 5s
+ retries: 12
+
+ target:
+ image: directus/directus:12.2.0
+ ports:
+ - 8056:8055
+ environment:
+ SECRET: "quickstart-target-secret"
+ DB_CLIENT: "pg"
+ DB_HOST: "target-db"
+ DB_PORT: "5432"
+ DB_DATABASE: "directus"
+ DB_USER: "directus"
+ DB_PASSWORD: "directus"
+ ADMIN_EMAIL: "admin@example.com"
+ ADMIN_PASSWORD: "quickstart"
+ ADMIN_TOKEN: "target-token"
+ depends_on:
+ target-db:
+ condition: service_healthy
+```
+
+```bash
+docker compose up -d
+```
+
+After a minute, `http://localhost:8055` (the source) and `http://localhost:8056` (the target) both serve a fresh Data Studio. Log in with `admin@example.com` / `quickstart`. The `ADMIN_TOKEN` values are static admin tokens the CLI will use.
+
+::callout{icon="i-lucide-info"}
+Both services use Directus 12.2.0 and PostgreSQL on purpose. Environment Sync requires the source and target to run the same Directus version, patch release included, and the same database vendor. In real projects, pin the same explicit version on every environment.
+::
+
+## Install the CLI
+
+```bash
+npm install -g @directus/cli@12
+d6s --version
+```
+
+Pinning the package to major version 12 selects the current Environment Sync CLI instead of the deprecated 9.x package previously published under this name. The package installs two commands that do the same thing: `directus-cli` and the short alias `d6s`. These docs use `d6s`.
+
+## Add a profile for each instance
+
+A profile pairs a name with an instance URL. Run this in the same directory as the compose file:
+
+```bash
+d6s profile add source --url http://localhost:8055 --token source-token
+```
+
+```cli
+◇ Saved profile "source" → http://localhost:8055
+◇ Saved a token for "source" to the credential store.
+```
+
+```bash
+d6s profile add target --url http://localhost:8056 --token target-token
+```
+
+That first command created `directus.config.json` in the current directory:
+
+```json
+{
+ "profiles": {
+ "source": { "url": "http://localhost:8055", "auth": { "type": "token" } },
+ "target": { "url": "http://localhost:8056", "auth": { "type": "token" } }
+ }
+}
+```
+
+Notice what is not in it: the tokens. URLs are project configuration you commit; credentials go to `~/.directus/credentials.json`, readable only by you. Confirm both connections work:
+
+```bash
+d6s profile test-connection source
+```
+
+```cli
+◇ Authenticated to http://localhost:8055 as Admin User (Administrator).
+```
+
+## Make something to sync
+
+In the source Studio at `http://localhost:8055`, create a collection called `articles` with two fields: `title` and `status`. This plays the part of a day's modeling work on a development instance.
+
+## Pull it into sync files
+
+Make the directory a git repository, then pull:
+
+```bash
+git init
+d6s sync pull --from source
+```
+
+```cli
+◇ Pulled from source — http://localhost:8055
+ Schema 1 collection → directus/default/schema
+ Configuration 14 records across 11 collections → directus/default/data
+```
+
+Your record counts may differ slightly; a fresh instance carries a handful of configuration records (the admin role, its policy, and the settings record) even before you touch it.
+
+Look at what appeared:
+
+```
+directus/default/
+ schema/ # one JSON file per collection, plus metadata.json
+ data/ # one JSON file per configuration resource, plus metadata.json
+```
+
+Open the `articles` file under `schema/` (file names carry a short content-hash suffix, so it's `articles_.json`). It's your collection, readable as JSON: the fields you just created, their types, their interface settings. This is what reviewers will see in pull requests. Commit it:
+
+```bash
+git add directus/ directus.config.json
+git commit -m "Baseline from source"
+```
+
+## Change something and pull again
+
+Back in the source Studio, add one more field to `articles`: `summary`. Then:
+
+```bash
+d6s sync pull --from source
+git diff
+```
+
+The diff touches one file, and inside it, only the new `summary` field. The CLI writes sync files deterministically: no timestamps, no reshuffling, nothing but your change. This makes the sync files reviewable and means repeating the pull while the source is unchanged leaves your working tree clean. Commit the field.
+
+```bash
+git add directus/
+git commit -m "Add articles.summary"
+```
+
+## Preview against the target
+
+The target still has only its initial Directus setup and does not have `articles`. Ask what pushing the sync files would do to it:
+
+```bash
+d6s sync diff --to target
+```
+
+```cli
+● Comparing ./directus/default with target — http://localhost:8056 (merge — creates and updates records, never deletes)
+● Schema — 1 change: 1 added, 0 modified, 0 deleted
++ collection articles (3 fields)
+● Configuration — no changes to push.
+```
+
+Read the plan: one collection to add, and the mode line tells you up front that `merge`, the default, never deletes anything. If the Configuration section lists a few records instead of "no changes", that's fine; fresh instances can differ slightly in their defaults. `diff` applied nothing; it's always safe to run.
+
+## Push
+
+```bash
+d6s sync push --to target
+```
+
+The push prints the same plan, then asks before applying: `Apply 1 schema change to target — http://localhost:8056?`. Confirm it:
+
+```cli
+● Schema applied.
+◇ Push complete. Applied 1 schema change to http://localhost:8056; schema hash verified. No configuration changes to push.
+```
+
+The "schema hash verified" line means the server confirmed that the target still matched the schema hash used to build the plan before applying it. If someone changes the target between planning and apply, the push stops instead of applying a stale plan.
+
+Open the target Studio at `http://localhost:8056`: the `articles` collection is there, fields and settings intact. That's the whole loop. Change one instance, pull to sync files, review in git, and push to another instance.
+
+If a push imports configuration records, it also writes `directus/default/id_map.json`, which records the target record that corresponds to each source record. Commit it when it changes; [How It Works](/guides/environment-sync/how-it-works#record-identity) explains why.
+
+## Push again
+
+```bash
+d6s sync push --to target
+```
+
+```cli
+● Pushing ./directus/default to target — http://localhost:8056 (merge — creates and updates records, never deletes)
+◇ target — http://localhost:8056 already matches ./directus/default — schema and configuration match; nothing to push.
+```
+
+Nothing to confirm, nothing applied. The CLI checked the target and proved it matches; it didn't assume. Re-running a completed push is safe, which is exactly what automation needs.
+
+## Clean up
+
+```bash
+docker compose down
+```
+
+Both instances and their databases are gone.
+
+## Where to go next
+
+- [How It Works](/guides/environment-sync/how-it-works): the mental model behind what you just did, and the safety rules around deletions and record identity.
+- [Common Workflows](/guides/environment-sync/common-workflows): the same loop applied to real situations, including adopting sync on an existing project.
+- [Reference](/guides/environment-sync/reference): every flag the commands take, including how to scope a pull to specific collections or resources.
diff --git a/content/guides/10.environment-sync/2.how-it-works.md b/content/guides/10.environment-sync/2.how-it-works.md
new file mode 100644
index 00000000..748b3e06
--- /dev/null
+++ b/content/guides/10.environment-sync/2.how-it-works.md
@@ -0,0 +1,143 @@
+---
+stableId: 5ec649aa-ca39-4d4c-b0f6-a10cbaf0cf38
+title: How It Works
+description: The mental model behind Environment Sync, including sync files as the source of truth, record identity across instances, how a push applies, and the safety rules every command follows.
+---
+
+Environment Sync never moves anything directly between two instances. The sync files sit in the middle, and two rules govern everything the commands do:
+
+1. **A pull overwrites the sync files in its requested scope with the source's current state.** Files outside that scope are left unchanged.
+2. **A push only applies what is in the sync files.** A push reads the files on disk, not the source instance. Work that never entered the sync files cannot ship.
+
+Everything else on this page is a consequence of those two rules.
+
+The CLI reads and writes the sync directory on disk. It never checks your git state, so a push applies the sync files as they exist, including uncommitted edits. That is also what lets the `d6s sync` wizard pull and push in one pass: the freshly pulled files push without a commit in between. Git provides review and history around the sync files. Keep the tree clean when you push so the files on disk match the reviewed commit.
+
+## The sync files
+
+A pull writes into a directory you commit (named `directus` by default, one subdirectory per [project](/guides/environment-sync/reference#directusconfigjson)):
+
+```
+directus/default/
+ schema/ # Schema, one JSON file per collection (_.json)
+ data/ # Configuration, one JSON file per resource (_.json)
+ id_map.json # written by pushes: which target record corresponds to each source record
+```
+
+The sync files are written deterministically: pulling twice with no instance changes produces byte-identical files and a clean working tree. `git diff` after a pull shows what changed on the instance and nothing else, so a schema change reads like any other code change in review.
+
+Both `schema/` and `data/` also contain a `metadata.json` file listing the files the CLI owns. A later pull can remove an owned file that became stale, but it never deletes a file outside that list. Hand-edited or corrupt sync files stop the command with a named error instead of applying invalid state.
+
+Profiles and per-project settings live in `directus.config.json` at the repository root. It contains URLs and scoping options, never credentials, so it is safe to commit. The [reference](/guides/environment-sync/reference#directusconfigjson) shows the full file.
+
+## Two independent axes
+
+A pull covers two independent things, and you can scope each without affecting the other:
+
+- **Schema**: collections, fields, relations. Scope it with `--collections` or `--exclude-collections`, or skip it with `--no-schema`.
+- **Configuration resources**: records from supported `directus_*` collections, including flows, roles, settings, and translations. Users are opt-in. Scope Configuration with resource flags like `--flows` or `--no-flows`.
+
+Scoping narrows what a pull overwrites from the source. Sync files outside the scope are left unchanged. The [pull scope matrix](/guides/environment-sync/reference#what-a-pull-touches) lists the effect of each flag combination, and [Common Workflows](/guides/environment-sync/common-workflows#promote-only-the-changes-that-are-ready) shows how to ship finished work while half-finished work stays out of the repository.
+
+## Record identity
+
+The same role or flow carries a different primary key on every instance, so a push has to decide which target record corresponds to each source record before it can update rather than duplicate. Two mechanisms decide, in order:
+
+- **The ID map.** Each push records its decisions in `id_map.json`: this source record corresponds to that target record. Later pushes look there first.
+- **Identifying fields.** A record not yet in the map is matched by stable fields: `name` for most named resources, `email` for a user, `flow` plus `key` for an operation, and `language` plus `key` for a translation. Access rules and permissions use their relationship fields. Panels have no such fields, which is why a first push into a look-alike target can duplicate them once.
+
+For an existing translation, the CLI replaces the source ID with the matching target ID and sends the complete record. Directus 12.2.0 and later accept an import update that repeats that record's current `language` and `key`, so both `merge` and `mirror` can update translation strings. A pair already owned by another translation still fails as a duplicate.
+
+When two target records could both be the match, the CLI asks you to choose in a terminal and refuses in CI. Ambiguity is never resolved by guessing. The question names both sides, links to the records when the Data Studio has a stable route for them, and explains what every choice will do:
+
+```cli
+directus_roles — 1 of 1
+./directus/default contains 1 role named "Editor".
+production — https://cms.example.com contains 2 matching roles.
+
+Role: "Editor" — sr1
+Source UI: https://source.example.com/admin/settings/roles/sr1
+Target UI 1: https://cms.example.com/admin/settings/roles/t1
+Target UI 2: https://cms.example.com/admin/settings/roles/t2
+
+Which target role does this represent?
+ ● Existing target role "Editor" — t1 (Same synced values; only the ID differs)
+ ○ Existing target role "Editor" — t2 (Merge updates the target; icon: local "edit", target "star")
+ ○ No existing role — create a new one on the target (Creates another "Editor" role)
+ ○ Abort push (Applies no remote changes)
+```
+
+The parenthetical after each existing candidate compares it to the sync files: identical values, or the fields that differ (here `icon`).
+
+Your answer lands in the ID map and is reused for later pushes between the same source and target URLs. That's why the map belongs in git: commit it whenever a push changes it, and teammates and CI inherit the decisions already made.
+
+One ID map serves any number of instances. Internally it is keyed by source and target URL, so pushing the same sync files to staging and production writes two independent sets of mappings; neither overwrites the other. Deleting the ID map does not change either instance, but matching starts over on the next push. Named records can usually match again by their identifying fields; records without them can duplicate. Repointing a profile at a new URL also starts a new set of mappings, because decisions stored for the old URL do not apply to the new one.
+
+## How a push applies
+
+After record identity is settled, an interactive push dry-runs Configuration, shows the full plan, and asks before applying unless you pass `--yes`. It then applies in two phases, Schema first:
+
+1. **Schema.** Before applying, the push re-checks that the target's schema still matches what the plan was computed against. If someone changed the target between preview and apply, the push stops rather than apply a stale plan.
+2. **Configuration.** Configuration records import in a single server-side transaction once the schema is in place.
+
+The two phases are not one transaction. If the Configuration push fails after Schema applied, the two phases can be out of sync, and the CLI says so plainly:
+
+```cli
+▲ Schema was applied, but the configuration push did not complete.
+✖ Could not reach https://cms.example.com.
+ Schema is already applied — re-run d6s sync push to retry the configuration push against an empty schema diff.
+```
+
+The guidance is the same in every partial-failure case: run `d6s sync diff` to see where the target actually stands, then push again. A re-run applies only what is still missing, and a completed push re-run reports "nothing to push" after verifying that against the target, not assuming it.
+
+## Push modes and deletions
+
+A push mode answers one question: what happens to things that exist on the target but not in your sync files?
+
+- **`merge`** (the default) creates and updates, and never deletes. Not in the Schema phase, not in the Configuration phase.
+- **`add`** only creates records; it never touches an existing one. Its Schema phase behaves exactly like `merge`: the mode only changes what happens to configuration records.
+- **`mirror`** makes the target match the sync files exactly, which means deleting what the sync files no longer contain, within whatever scope they cover.
+
+Deleting always requires its own explicit consent, separate from confirming the push. Interactively, a mirror push lists what would be lost and asks you to type the profile name. Non-interactively it requires the `--dangerously-allow-delete` flag; `--yes` never authorizes a deletion. The gate is a backstop as well as a policy: even if a non-deleting push somehow carried a deletion, it would still be refused without consent.
+
+```bash
+d6s sync push --to target --mode mirror --dangerously-allow-delete
+```
+
+```cli
+● Pushing ./directus/default to target — http://localhost:8056 (mirror — INCLUDES DELETIONS)
+● Configuration — 3 changes: 0 created, 0 updated, 3 deleted
+~ directus_flows +0 new ~0 updated ✖1 deleted (dd7b3073-be2f-4dfa-aebf-79ab046fc619)
+~ directus_roles +0 new ~0 updated ✖2 deleted (2121f751-31b2-4c34-93c0-64516f03b6f1, 86568388-e40d-4100-835c-f2238f57c95a)
+
+Apply 3 configuration changes to target — http://localhost:8056?
+ ● Yes / ○ No
+```
+
+The mode banner says `INCLUDES DELETIONS` up front. The plan names each record that would go, and the apply prompt still asks — `--dangerously-allow-delete` is consent to delete, not a skip of the ordinary confirmation. Pass `--yes` as well only when you mean both.
+
+Because a mirror push makes the target match the sync files _exactly_, run it against a freshly pulled state. A mirror from stale sync files applies the stale state, including deleting things that only look obsolete because the files are old.
+
+## The compatibility gate
+
+Schema comparison requires two things to match: the exact Directus version recorded in the sync files, patch release included, and the target's database vendor. The server refuses the comparison when either differs. Some Directus patches have changed the snapshot format, and database vendors describe column types differently.
+
+```cli
+✖ Version mismatch: the snapshot was pulled from Directus 12.1.1, but the target runs 12.2.0.
+ The server requires an exact version match for schema diffs — historically some patches are breaking. Align both instances (re-pull if the source was upgraded), or pass --allow-drift to proceed anyway.
+```
+
+The CLI can name a known version mismatch before sending the snapshot. It cannot pre-check the target's database vendor, so a vendor mismatch comes back as an incompatible-snapshot refusal with the server's reason attached.
+
+`--allow-drift` sends the server's `force` bypass for either mismatch and prints a `▲ Compatibility check bypassed` warning. It does not translate schema across Directus versions or database vendors, so read the plan closely. Projects configured with `"schema": false` skip the Schema phase and this comparison; the 12.2.0 minimum still applies.
+
+## The safety model
+
+The rules above add up to a small set of promises, each of which you can watch hold in the [Quickstart](/guides/environment-sync/quickstart):
+
+- **`pull` is read-only on the source.** Every request it makes is a read; it changes nothing on the instance it snapshots. (The one exception: a profile that authenticates with a saved login session refreshes that session when it is close to expiring.)
+- **`diff` changes no Schema or Configuration records.** The Schema comparison is a preview, and the Configuration plan comes from the target server dry-running the import inside a transaction and rolling it back, so the plan is the server's own answer, not a client-side guess. On PostgreSQL, that dry run can advance an integer primary-key sequence and leave harmless gaps.
+- **Deletions are gated.** Only `mirror` deletes, always behind its own consent, and `--yes` never covers it.
+- **Identity is never guessed.** An ambiguous record match prompts in a terminal and refuses in CI.
+- **Recognized secret fields are stripped.** Built-in secret columns and fields marked concealed, hashed, or encrypted are removed during pull. Free-form flow configuration has [one warned blind spot](/guides/environment-sync/secrets-and-limitations#the-one-blind-spot).
+- **Failures are loud.** Hand-edited sync files, incomplete pulls, compatibility mismatches, and unreachable instances stop the command with a named error instead of degrading silently. A pull the source itself left incomplete is marked as such and refused at mirror push.
diff --git a/content/guides/10.environment-sync/3.common-workflows.md b/content/guides/10.environment-sync/3.common-workflows.md
new file mode 100644
index 00000000..9e6829ff
--- /dev/null
+++ b/content/guides/10.environment-sync/3.common-workflows.md
@@ -0,0 +1,231 @@
+---
+stableId: b0bd4f6a-0708-4681-86dc-c482895372c1
+title: Common Workflows
+description: End-to-end walkthroughs of the most common Environment Sync workflows, from promoting changes to production to adopting sync on an existing project, rolling back, and recovering a drifted environment.
+---
+
+These workflows cover most day-to-day use of Environment Sync: promoting changes to production (all of them, or just the ones that are ready), starting to use sync on a project that already exists, undoing a bad push, re-aligning an instance after manual changes, and standing up a new instance from the sync files. The concepts behind each step live in [How It Works](/guides/environment-sync/how-it-works); every flag is in [Reference](/guides/environment-sync/reference).
+
+## Promote changes from development to production
+
+You model in the Data Studio on a development instance. Production receives the state captured in sync files that were reviewed in git.
+
+1. Make your changes on the development instance: collections, fields, flows, permissions.
+
+2. Pull them into the repository and commit:
+
+ ```bash
+ d6s sync pull --from dev
+ git add directus/
+ git commit -m "Add author bio fields"
+ ```
+
+ The sync files are deterministic, so the commit shows your change and nothing else. On a development instance other people also use, a scoped pull (`--collections posts`, or a resource flag like `--flows`) lets you overwrite only the part you are ready to review.
+
+3. Open a pull request. Reviewers read the change as plain JSON diffs. A CI check can add the target's view of the same change:
+
+ ```bash
+ d6s sync diff --to production --json
+ ```
+
+4. On merge, apply:
+
+ ```bash
+ d6s sync push --to production --yes
+ ```
+
+ The default `merge` mode creates and updates but never deletes. A second run reports nothing to push.
+
+::callout{icon="i-lucide-triangle-alert" color="warning"}
+**Removals need `mirror`.** A change that deletes a field or a record does not propagate under `merge`. Push with `--mode mirror` and pass its [deletion gate](/guides/environment-sync/reference#deletion-gates), and run a full pull first so the mirror applies current state, not a stale tree.
+::
+
+## Promote only the changes that are ready
+
+A shared development or staging instance usually carries more than one piece of work at a time. Say the `posts` changes are ready to ship, and half-finished `authors` changes are not. Scope the pull to what ships:
+
+```bash
+d6s sync pull --from staging --collections posts
+```
+
+What that pull just did:
+
+- The `posts` Schema sync files were overwritten with the current source state.
+- The `authors` Schema sync files were outside the pull scope and stayed unchanged on disk. They still hold what the last full pull captured, which is the state production already runs. The half-finished `authors` work never entered the sync files.
+- The default Configuration sync files were also overwritten because a collection scope only narrows Schema. Their source state had not changed, so the overwritten files were byte-identical and `git status` shows only the `posts` files. If a flow had changed on staging, its file would appear too; exclude it with `--no-flows`.
+- Dependent resources cannot be excluded individually. Permissions are included with policies, and operations are included with flows. A teammate's new permission records therefore appear whenever the pull includes policies.
+
+Commit, then diff and push as usual:
+
+```bash
+git add directus/
+git commit -m "Add post fields"
+d6s sync diff --to production
+d6s sync push --to production
+```
+
+A push always applies the full sync project on disk. The `posts` change applies. The `authors` files already match production, so nothing happens there. The unfinished `authors` work is absent from the sync files, so it cannot ship, no matter what state staging is in.
+
+For a configuration-only change, flip the scope: select the resource type and skip schema.
+
+```bash
+d6s sync pull --from staging --flows --no-schema
+```
+
+The [What a pull touches](/guides/environment-sync/reference#what-a-pull-touches) table shows exactly which sync files each combination overwrites from the source and which it leaves unchanged.
+
+::callout{icon="i-lucide-triangle-alert" color="warning"}
+**Selection is by collection and resource type, not by record.** `--collections posts` can separate finished posts work from unfinished authors work, but two changes inside the same collection travel together, and `--flows` takes every flow, not just one. If unrelated work shares a collection or a resource type, ship it together or wait. While the sync files hold a partial picture, avoid `mirror`: it would apply the stale remainder too.
+::
+
+## Adopt Environment Sync on an existing project
+
+Most projects don't start from an empty instance. You have a development instance, a production instance, and months of changes applied to each by hand. Here's how to get from that to a synced setup.
+
+1. Decide which instance is closest to the state you want everywhere. Usually that's production: it's the environment you've been careful with.
+
+2. Pull it as your baseline and commit:
+
+ ```bash
+ d6s profile add production --url https://cms.example.com # offers to save a credential; or pass --token
+ d6s sync pull --from production
+ git add directus/ directus.config.json
+ git commit -m "Baseline from production"
+ ```
+
+3. See how far each other environment has drifted:
+
+ ```bash
+ d6s profile add staging --url https://staging.example.com
+ d6s sync diff --to staging
+ ```
+
+ The first diff on a long-lived pair of instances can be long. Read it as an inventory of drift, not a to-do list: everything listed is a place where staging and production genuinely disagree, accumulated over however long the two were maintained by hand.
+
+4. Bring the environment in line gradually. A `merge` push adds and updates what staging is missing without deleting anything, so staging-only experiments survive:
+
+ ```bash
+ d6s sync push --to staging
+ ```
+
+ Once nothing on staging is worth keeping outside the sync files, a `mirror` push finishes the job and makes it match exactly.
+
+5. Expect a few identity questions on the first push. Staging and production often hold records that are plausibly the same one (two roles named "Editor", two flows built from the same template). The CLI asks instead of guessing; answer, then commit the updated `id_map.json`. Those decisions are reused on later pushes between the same source and target URLs.
+
+From then on, the project runs the [promote workflow](#promote-changes-from-development-to-production): change on dev, pull, review, push.
+
+::callout{icon="i-lucide-info"}
+Nervous about the first push against a real instance? Rehearse the whole loop against throwaway instances first: the [Quickstart](/guides/environment-sync/quickstart) sandbox is exactly that, and adding a profile for a scratch instance to a real repository is harmless.
+::
+
+## Roll back a bad push
+
+A change made it to production and turned out to be wrong. Git history records every reviewed state of the sync files, so rollback is a git operation followed by a push. One asymmetry matters:
+
+- If the bad change **modified** something, reverting the commit and pushing with `merge` restores the old values.
+- If the bad change **added** something, `merge` cannot undo it: after the revert, the field or record is absent from your sync files, and merge never deletes what the sync files don't mention. Removal is a deletion, and deletions need `mirror`.
+
+Say the bad commit added a `summary` field and a notification flow, and tweaked the note on `articles.title`. Revert it, then preview the undo in mirror mode (the default `merge` preview plans no deletions, so it cannot show you a removal):
+
+```bash
+git revert
+d6s sync diff --to production --mode mirror
+```
+
+```cli
+● Comparing ./directus/default with production — https://cms.example.com (mirror — INCLUDES DELETIONS)
+● Schema — 2 changes: 0 added, 1 modified, 1 deleted
+✖ DELETE field articles.summary
+~ field articles.title (meta.note)
+● Configuration — 1 change: 0 created, 0 updated, 1 deleted
+~ directus_flows +0 new ~0 updated ✖1 deleted (f7)
+```
+
+The `~` line restores the old note. The `✖ DELETE` and `✖1 deleted` lines are the undo, and they should name exactly what the bad change added, and nothing else. If anything else shows up for deletion, your sync files are stale: stop, run a full pull from production on a scratch branch, read that diff to see what production actually holds, and fold anything worth keeping into the sync files before mirroring.
+
+```bash
+d6s sync push --to production --mode mirror
+```
+
+The push repeats the plan, then the [deletion gate](/guides/environment-sync/reference#deletion-gates) demands typed consent:
+
+```cli
+This push permanently deletes 1 configuration record and 1 schema item from production. Type "production" to confirm:
+```
+
+In automation, the same push requires `--dangerously-allow-delete` instead.
+
+::callout{icon="i-lucide-triangle-alert" color="warning"}
+**Undoing an added field deletes its content.** Mirroring away `summary` drops the column and everything editors typed into it since the bad push. The sync files cover configuration; only a database backup covers content. Back up the target first if that content matters.
+::
+
+When the bad change is tangled up with good ones, fixing forward is often simpler than reverting: correct it on the development instance, pull, and promote the fix like any other change.
+
+## Rebase an environment from production after drift
+
+Sometimes production changes outside the deployment path, usually an urgent manual fix. Staging and the sync files no longer reflect reality. Bring the fix into git, then re-align the lower instance. This is what `mirror` is for: making an instance match the sync files exactly.
+
+1. Pull the full state from production. The manual change appears as an ordinary git diff, which is your record of what the hotfix actually was:
+
+ ```bash
+ d6s sync pull --from production
+ git diff
+ git add directus/
+ git commit -m "Adopt production hotfix"
+ ```
+
+2. Preview what re-aligning staging would mean:
+
+ ```bash
+ d6s sync diff --to staging --mode mirror
+ ```
+
+ Read the deletions closely. Anything that exists only on staging and falls inside the sync's scope is on the list. (The mode matters: a default `merge` preview plans no deletions, so only a mirror diff shows what re-aligning would remove.)
+
+3. Make staging match the sync files:
+
+ ```bash
+ d6s sync push --to staging --mode mirror
+ ```
+
+ Interactively, the push names the losses and asks you to type the profile name; in automation it requires `--dangerously-allow-delete`. See [deletion gates](/guides/environment-sync/reference#deletion-gates).
+
+::callout{icon="i-lucide-triangle-alert" color="warning"}
+**Mirror removes staging-only work in scope.** If staging holds experiments you want to keep, pull that work into a branch first, or use `merge` and clean up by hand.
+::
+
+## Stand up a new environment
+
+Going from an empty instance to a working copy of your project's shape:
+
+1. Provision a fresh Directus instance and create its admin account as usual.
+
+2. Add a profile for it:
+
+ ```bash
+ d6s profile add staging --url https://staging.example.com --token
+ ```
+
+3. Preview, then push the sync files:
+
+ ```bash
+ d6s sync diff --to staging
+ d6s sync push --to staging
+ ```
+
+ Schema applies first, then the Configuration records import.
+
+4. Commit the updated `id_map.json`. The push records which target record corresponds to each source record, and the ID map is how every later push updates records instead of duplicating them.
+
+Things to expect on a first push:
+
+- **Identity questions.** If the target already holds records the CLI cannot tell apart from the source records (two policies named "Administrator" is the classic case), an interactive push asks you to choose. Commit the ID map so later pushes between the same source and target URLs reuse the answer. See [record identity](/guides/environment-sync/how-it-works#record-identity).
+- **Recognized secrets stay behind.** API keys in settings and fields marked concealed, hashed, or encrypted are stripped. Set them on the new instance directly. Free-form flow request headers are not stripped, so review the warning and the operation files before committing them.
+
+::callout{icon="i-lucide-triangle-alert" color="warning"}
+**Extensions do not sync, and nothing warns about them.** A field built on a custom interface, or a flow using a custom operation, pushes silently to a target that may not have that extension installed, and arrives broken in the Studio until it is. Deploy your extensions to the target before pushing schema or flows that depend on them.
+::
+
+::callout{icon="i-lucide-info"}
+Push the full sync project to a fresh target, not a scoped slice. A partial sync project whose relations point at collections outside the scope can fail to apply on an instance that has nothing else yet. The pull warns about these references when it writes the sync files.
+::
diff --git a/content/guides/10.environment-sync/4.ci-and-automation.md b/content/guides/10.environment-sync/4.ci-and-automation.md
new file mode 100644
index 00000000..896d333e
--- /dev/null
+++ b/content/guides/10.environment-sync/4.ci-and-automation.md
@@ -0,0 +1,154 @@
+---
+stableId: d5ad7a4c-dd80-4394-af25-333dabfeeaf1
+title: CI & Automation
+description: Post the production diff on every pull request and push on merge, with tokens from environment variables, machine-readable JSON reports, and explicit flags in place of prompts.
+---
+
+Automating Environment Sync buys you two things: every pull request shows what merging it would change on production, and merging applies it without anyone running a command. This page builds that pipeline and covers the rules unattended runs follow.
+
+## The non-interactive contract
+
+The CLI treats a non-empty `CI` variable as non-interactive (locally, `--no-interactive` forces the same behavior). Where the interactive CLI would ask a question, a non-interactive run refuses instead:
+
+- An **ambiguous record match** (two target records that could both correspond to the source record) fails the command rather than picking a candidate. Resolve it with an interactive push and commit the ID map; later CI pushes between the same source and target URLs reuse the answer.
+- **Deletions** happen only with `--dangerously-allow-delete`. A `mirror` push without it refuses before changing anything on the target.
+- `--yes` confirms an ordinary, non-destructive apply. It never authorizes a deletion.
+
+Commands exit `0` on success and `1` on any refusal or failure; there are no other exit codes. Anything finer-grained (which kind of failure, how many changes) comes from the JSON report, not the exit code.
+
+## Credentials
+
+Pass tokens through environment variables named `DIRECTUS__TOKEN`, the profile name uppercased. Profile names use letters, numbers, and underscores, so the mapping is mechanical: profile `production` reads `DIRECTUS_PRODUCTION_TOKEN`, profile `staging_eu` reads `DIRECTUS_STAGING_EU_TOKEN`.
+
+The credential store saved on a developer machine is never read when `CI` is non-empty; tokens come from the environment only.
+
+## JSON reports
+
+Add `--json` and stdout carries exactly one machine-readable report per command. Warnings (stripped secret fields, compatibility checks bypassed with `--allow-drift`, flow headers written verbatim) still go to stderr, so your logs keep them while stdout stays parseable. A failure puts an error report on stdout **in place of** the success report, shaped `{"error": {"code": …}}` with a stable `code` naming the failure class. Redirecting stdout to a file therefore captures the error instead of showing it; pipe through `tee` so the failure is visible in your logs too.
+
+The fields automation usually keys on:
+
+- **`changes`** (diff): `true` when the push would do anything, including when Configuration has ambiguous target matches.
+- **`data.reconciliation.ambiguous`** (diff): the number of Configuration records that need an identity choice. **`data.reconciliation.dependent`** counts records waiting on those choices. A non-interactive push refuses this state, so it is a real difference for your pipeline to surface, not noise.
+- **`applied`** (push): `true` when the push sent an apply or import to the target.
+- **`data`** is `null` when the project has no Configuration files at all, so reach through it (`report.data?.…`) rather than assuming an object.
+- **`data.unchanged`** (diff) counts matched records whose values already agree. The server reports every matched record under `resultsByCollection[…].existing` whether or not it differs, so an honest "updated" count is `existing` minus `unchanged`.
+
+`d6s sync diff` exits `0` whether or not differences exist; it fails only when it cannot produce an answer. Distinguish those two: check for `error` first, then gate pipeline behavior on `changes` rather than the exit code. The [reference](/guides/environment-sync/reference#json-reports) documents every report field.
+
+## A GitHub Actions pipeline
+
+One workflow, two jobs: pull requests get the production diff as a comment, and merges to `main` apply the reviewed sync files. Store the token as the `DIRECTUS_PRODUCTION_TOKEN` Actions secret and the expected instance URL as the `DIRECTUS_PRODUCTION_URL` Actions variable.
+
+```yaml
+name: environment-sync
+
+on:
+ pull_request:
+ push:
+ branches: [main]
+
+jobs:
+ diff:
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ - run: npm install -g @directus/cli@12
+ - name: Verify the production profile URL
+ env:
+ EXPECTED_URL: ${{ vars.DIRECTUS_PRODUCTION_URL }}
+ run: |
+ actual=$(jq -r '.profiles.production.url // ""' directus.config.json)
+ if [ -z "$EXPECTED_URL" ] || [ "$actual" != "$EXPECTED_URL" ]; then
+ echo "Unexpected production profile URL: $actual"
+ exit 1
+ fi
+ - name: Diff against production
+ shell: bash
+ run: d6s sync diff --to production --json | tee diff-report.json
+ env:
+ DIRECTUS_PRODUCTION_TOKEN: ${{ secrets.DIRECTUS_PRODUCTION_TOKEN }}
+ - name: Comment the result on the PR
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const report = JSON.parse(require('fs').readFileSync('diff-report.json', 'utf8'));
+ const sum = (key) =>
+ Object.values(report.data?.resultsByCollection ?? {}).reduce((total, result) => total + result[key].length, 0);
+ const updated = sum('existing') - (report.data?.unchanged ?? 0);
+ const schema = report.schemaSkipped
+ ? 'Schema: skipped.'
+ : `Schema: ${report.added} added, ${report.modified} modified, ${report.deleted} deleted.`;
+ const body = report.changes
+ ? `**Environment Sync**: merging this PR changes production. ${schema} ` +
+ `Configuration: ${sum('new')} created, ${updated} updated, ${sum('deleted')} deleted; ` +
+ `${report.data?.reconciliation?.ambiguous ?? 0} ambiguous matches.`
+ : '**Environment Sync**: production already matches this branch.';
+ await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body });
+
+ push:
+ if: github.event_name == 'push'
+ runs-on: ubuntu-latest
+ concurrency:
+ group: environment-sync-production
+ cancel-in-progress: false
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ - run: npm install -g @directus/cli@12
+ - name: Verify the production profile URL
+ env:
+ EXPECTED_URL: ${{ vars.DIRECTUS_PRODUCTION_URL }}
+ run: |
+ actual=$(jq -r '.profiles.production.url // ""' directus.config.json)
+ if [ -z "$EXPECTED_URL" ] || [ "$actual" != "$EXPECTED_URL" ]; then
+ echo "Unexpected production profile URL: $actual"
+ exit 1
+ fi
+ - name: Push to production
+ shell: bash
+ run: d6s sync push --to production --yes --json | tee push-report.json
+ env:
+ DIRECTUS_PRODUCTION_TOKEN: ${{ secrets.DIRECTUS_PRODUCTION_TOKEN }}
+ - name: Commit the updated ID map
+ if: ${{ !cancelled() }}
+ run: |
+ if [ -n "$(git status --porcelain -- 'directus/*/id_map.json')" ]; then
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add 'directus/*/id_map.json'
+ git commit -m "Update sync ID map"
+ git push
+ fi
+```
+
+Two things to know before enabling the push job:
+
+- **Run the first push interactively, locally.** The first push into a target tends to raise the identity questions described in [How It Works](/guides/environment-sync/how-it-works#record-identity), and CI refuses them. Answer them from a terminal and commit `id_map.json` before enabling the push job.
+- **The ID map commit-back step matters.** A push that creates records adds entries to `id_map.json`. If CI doesn't commit them, the next push re-matches those records from scratch, and records without an identifying field (panels) can duplicate. It runs on `!cancelled()` rather than only on success, because a push that imports records and then fails still wrote real mappings worth keeping.
+- **The bot needs somewhere to push.** Branch protection that requires pull requests blocks `github-actions[bot]` from committing the ID map; give it an exemption or have the step open a pull request instead. Commits made with `GITHUB_TOKEN` never trigger another workflow run, so this cannot loop.
+
+The URL check must run before any step receives the production token. It prevents a pull request from changing the `production` profile to another host and sending the token there. The sample skips pull requests from forks because GitHub does not expose repository secrets to them; run local or tokenless checks for those contributions instead.
+
+A scheduled pull is the same pattern in reverse: run `d6s sync pull --from staging --json` on a cron trigger and commit the result. A clean working tree means nothing changed on the instance; a diff is drift, arriving as a reviewable commit or PR instead of a surprise.
+
+## Mirror pushes in automation
+
+A `mirror` push deletes, so it additionally requires `--dangerously-allow-delete`:
+
+```bash
+d6s sync push --to staging --mode mirror --yes --dangerously-allow-delete
+```
+
+Reserve this for pipelines that rebuild disposable environments, and keep production pushes on the default `merge` unless a human reviewed the deletions in the diff. The flag name is deliberate.
diff --git a/content/guides/10.environment-sync/5.secrets-and-limitations.md b/content/guides/10.environment-sync/5.secrets-and-limitations.md
new file mode 100644
index 00000000..781ceb34
--- /dev/null
+++ b/content/guides/10.environment-sync/5.secrets-and-limitations.md
@@ -0,0 +1,67 @@
+---
+stableId: ffd6edef-6cd2-499f-b807-5e9e6e1ef513
+title: Secrets & Limitations
+description: How Environment Sync keeps secret values out of the files you commit, the one exception to review before publishing a repository, and what the tool deliberately does not do.
+---
+
+## How secrets are handled
+
+A pull writes real configuration records to sync files: settings, user accounts, flow definitions, and more. If any carried a secret value, it would land in JSON files you commit, and git history would keep it in every clone of the repository. Environment Sync strips secret values during pull:
+
+- **Built-in secret fields** (password hashes, tokens, 2FA seeds, license and AI keys) are always removed from the sync files.
+- **Fields you created and marked concealed, hashed, or encrypted** are removed too. Every pull that includes Configuration fetches the server's complete field list and drops any flagged value, printing a line naming each dropped field. A value missing from a sync file is protection, not data loss.
+
+Because the field list is fetched separately from Schema, the check also covers pulls scoped to a few collections and pulls that skip Schema entirely. If the check itself cannot run, the pull stops rather than continue unprotected.
+
+The field definition still syncs as Schema: the field and its concealed setting arrive on the target. Only the value stays behind; set the real secret on each instance directly.
+
+### Why strip instead of sync
+
+The server never hands out the real value for these fields: concealed fields read back as `**********`, hashed fields as the hash. Writing that value to a sync file and pushing it would overwrite the target's working secret with a mask. Stripping protects both the repository and the target.
+
+### The one blind spot
+
+::callout{icon="i-lucide-triangle-alert" color="warning"}
+**Flow request options are written verbatim.** A secret pasted into free-form flow configuration (an Authorization header, a `?api_key=` URL parameter, a token in the request body) has no "this is secret" marker for the CLI to check, and legitimate values have to sync. The pull warns you by operation name when it sees headers, a URL carrying a query string or embedded credentials, or a body — but it cannot recognize a secret embedded in a plain URL path, and the value always goes into the sync file as-is. Review those files before committing and before publishing a repository.
+::
+
+## System collections that do not sync
+
+None of these sync today. Some are shared configuration that a future release could take on; the rest are per-instance state that a sync should never touch.
+
+| Collection | Why it doesn't sync |
+| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `directus_presets` | Bookmarks, saved layouts, and Insights presets mix shared configuration with personal preference. A future release may sync the shared part. |
+| `directus_extensions` | The enabled/disabled state of installed extensions is tied to what is physically deployed on each instance. Deploy extensions to the target before pushing schema or flows that depend on them; the push itself does not warn. |
+| `directus_files` | File-interface **fields** sync as Schema; file records and the binaries behind them are their own workstream. |
+| `directus_comments` | Content comments; per-instance state. |
+| `directus_activity` | The audit log; per-instance state. |
+| `directus_revisions` | Change history; per-instance state. |
+| `directus_versions` | Content-versioning drafts; per-instance state. |
+| `directus_notifications` | User notifications; per-instance state. |
+| `directus_shares` | Public share links; per-instance state. |
+| `directus_sessions` | Active login sessions; per-instance state. |
+| `directus_migrations` | The database migration ledger. The CLI never runs migrations or changes the Directus version. |
+| `directus_webhooks` | Deprecated in Directus; superseded by flows. |
+
+## What it does not do
+
+| Won't | Because / instead |
+| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Sync **records in your own collections** | Environment Sync moves the shape of a project and its configuration. Content sync is deferred to a future release. |
+| **Undo** a push | Revert the commit and push again; removals need `mirror`. See [Roll back a bad push](/guides/environment-sync/common-workflows#roll-back-a-bad-push). Only a database backup covers content. |
+| **Auto-expand** a collection-scoped pull | A collection scope pulls exactly what you name. Dangling references produce a warning; widen `--collections` yourself. Resource dependencies are handled separately and included by default. |
+| Translate schema **across versions** | A version mismatch refuses the command; `--allow-drift` bypasses the compatibility gate but converts nothing. |
+| Translate schema **across database vendors** | A vendor mismatch refuses the command; `--allow-drift` bypasses the same gate but does not rewrite vendor-specific column types. |
+| Wrap Schema and Configuration in **one transaction** | Schema applies first, then Configuration. A failed import re-runs Configuration alone; see [How It Works](/guides/environment-sync/how-it-works#how-a-push-applies). |
+| Select **individual records** | Resource selection is by type (`--roles`), never by record. |
+| Model **code-first** | Model in the Data Studio and pull. Hand edits to the JSON files are workable if you only push; a subsequent pull in scope overwrites them. |
+| **Seal the Configuration plan** against target drift | Only the Schema apply is sealed against the target changing between preview and apply. The Configuration preview is the server's own dry-run answer, but it is advisory. |
+
+## Known limitations
+
+- **Unlicensed custom permission rules are unavailable to pull.** On an instance without a license, the API hides stored permissions that use filters, field restrictions, validation, or presets. The pull detects the missing records and marks the result incomplete: `merge` and `add` push normally, while `mirror` refuses. Some hidden rows may just be stored copies of the built-in permissions every app-access policy gets automatically — harmless to lose, since the server recreates them — but the CLI cannot verify that, so the incomplete marking stands. License the source instance and re-pull to include authored rules.
+- **Users without an email address fail import.** The server's import validation rejects a user with no email, so a `--users` push containing one fails before any Configuration changes apply. Give the account an email on the source, or remove it, first.
+- **Panels can duplicate on a first sync into a look-alike target.** Panels have no name to match on, so pushing into a target that already holds equivalent panels (seeded from the same template) can create them a second time. The ID map prevents repeats after that first push.
+- **A mirror push of users does not protect your own account.** If users are in the sync files and the account the push authenticates with is absent from them, a `mirror` push orders its deletion like any other record; the CLI has no self-protection, and whether the server refuses is up to the server. When you sync users, make sure the sync files include the accounts your pushes run as.
+- **A Configuration diff can leave harmless gaps in PostgreSQL integer IDs.** The target server dry-runs the import and rolls it back, but PostgreSQL sequences do not roll back. No records are created or changed; only a sequence such as the one for `directus_permissions` can advance.
diff --git a/content/guides/10.environment-sync/6.reference.md b/content/guides/10.environment-sync/6.reference.md
new file mode 100644
index 00000000..530ee73c
--- /dev/null
+++ b/content/guides/10.environment-sync/6.reference.md
@@ -0,0 +1,432 @@
+---
+stableId: 342cc194-f6cc-49f8-9455-b4ae10c2e9da
+title: Reference
+description: Every Environment Sync command and flag, the directus.config.json format, credential resolution, scope tables, push modes, deletion gates, and JSON report formats.
+---
+
+Everything on this page is lookup material. If you're learning the tool, start with the [Quickstart](/guides/environment-sync/quickstart) and [How It Works](/guides/environment-sync/how-it-works) instead.
+
+## Commands
+
+| Command | What it does |
+| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
+| `d6s profile add` | Create a profile and optionally save a credential |
+| `d6s profile update` | Change a profile's name, URL, or saved credential |
+| `d6s profile list` | List configured profile names and URLs |
+| `d6s profile test-connection` | Connect with a profile or URL and print who you are on the instance |
+| `d6s profile remove` | Remove a profile and its saved credential |
+| `d6s sync pull` | Write a source instance's Schema and Configuration to sync files |
+| `d6s sync diff` | Show what a push would change on the target; applies nothing |
+| `d6s sync push` | Apply the sync files to a target instance |
+| `d6s sync` | Interactive wizard: prompts for source and target (plus project and mode when the configuration leaves them open), then pulls and pushes |
+
+Install `@directus/cli@12`. It provides `directus-cli` and the equivalent short alias `d6s`.
+
+Global flags, available on every command:
+
+| Flag | Effect |
+| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
+| `--json` | One machine-readable report on stdout; human status stays off stdout. Also disables prompts, so the non-interactive rules apply |
+| `--no-color` | Disable colored output |
+| `--no-interactive` | Disable prompts; behave as in CI |
+| `--config ` | Path to `directus.config.json` (default: found by walking up from the current directory) |
+
+## `d6s profile add`
+
+```bash
+d6s profile add [name] [--url ] [--token ]
+```
+
+| Flag | Effect |
+| ----------------- | ------------------------------------------------------------- |
+| `--url ` | Directus instance URL |
+| `--token ` | Static token to save to the credential store for this profile |
+
+An existing profile name is refused; use `profile update` to change it. Run without arguments for prompts, which also offer to save a credential (paste a static token, or log in with email and password to save a session; saved sessions refresh themselves before they expire). Profile names use letters, numbers, and underscores. URLs with embedded credentials, query strings, or fragments are refused because the URL is stored in project configuration.
+
+## `d6s profile update`
+
+```bash
+d6s profile update [name] [--name ] [--url ] [--token ] [--yes]
+```
+
+| Flag | Effect |
+| ----------------- | -------------------------------------------------------------------------------- |
+| `--name ` | Rename the profile; run this separately from URL or credential changes |
+| `--url ` | Point the profile at a new Directus instance; keeps the current URL when omitted |
+| `--token ` | Replace the saved credential with a static token |
+| `--yes` | Skip confirmation when changing the profile name or URL |
+
+Renaming moves the saved credential to the new profile name and changes the environment variable the profile reads. Changing the URL clears the credential saved for the old URL; a `DIRECTUS__TOKEN` environment variable follows the profile to the new URL. The command states these effects before asking for confirmation.
+
+## `d6s profile list`
+
+```bash
+d6s profile list
+```
+
+Prints each configured profile name and URL. Credentials are never shown.
+
+## `d6s profile test-connection`
+
+```bash
+d6s profile test-connection [name] [--url ] [--token ]
+```
+
+| Flag | Effect |
+| ----------------- | ----------------------------------------------------- |
+| `--url ` | Test a URL directly, without a profile or config file |
+| `--token ` | Override the resolved token |
+
+Connects and prints who the credential authenticates as:
+
+```cli
+◇ Authenticated to https://cms.example.com as Admin User (Administrator).
+```
+
+Pass either a profile name or `--url`, never both. Without a stored or environment credential, an interactive run asks for one; a non-interactive run tells you which token flag or environment variable to set.
+
+## `d6s profile remove`
+
+```bash
+d6s profile remove [name] [--yes]
+```
+
+Removes the profile and clears its saved credential after confirmation. `--yes` skips that confirmation; non-interactive runs require it.
+
+## `d6s sync pull`
+
+```bash
+d6s sync pull --from [scope flags]
+```
+
+| Flag | Effect |
+| ------------------------------ | --------------------------------------------------------------------------------- |
+| `--from ` (required) | Source profile name |
+| `--collections ` | Schema scope: only these collections (comma-separated) |
+| `--exclude-collections ` | Schema scope: all collections except these |
+| `--no-schema` | Skip Schema entirely; Configuration resources only |
+| `--` | Select only the named resources, e.g. `--flows --roles` (plus their dependencies) |
+| `--no-` | Keep the default resource set but exclude one, e.g. `--no-flows` |
+| `--all` | Every configuration resource, including users |
+| `--no-deps` | Do not add prerequisites; resources owned by a selected parent remain included |
+| `--project ` | Project to sync (default: `default`) |
+
+A bare pull includes every selectable resource except users. This means translations sync by default. Pass `--no-translations` to exclude them, or `--translations` to pull only translations.
+
+The selectable resources are `roles`, `policies`, `flows`, `dashboards`, `settings`, `folders`, `users`, and `translations`. The CLI automatically includes `access`, `permissions`, `operations`, and `panels` with their parent resources (see [the dependency table](#configuration-resources)). Positive selection (`--flows`) cannot be combined with `--all` or with `--no-` flags. An exclusion that a retained resource depends on is refused rather than silently overridden: `--no-policies` alone fails because `roles` requires policies and would pull them back — pass `--no-roles` as well, or keep policies. Resource selection never narrows the schema; the two axes are scoped independently.
+
+Two warnings a scoped pull can raise, neither of which widens the scope for you:
+
+- **Out-of-scope references.** The scoped sync files point at something you omitted (a relation target, a group parent, a many-to-any collection). Pushing those files to a fresh target can fail; add the missing collections to `--collections` yourself.
+- **A name the server didn't return.** A `--collections` name absent from the returned Schema (usually a typo) is named in a warning. The files for the rest of the requested scope are still overwritten, but never silently.
+
+## `d6s sync diff`
+
+```bash
+d6s sync diff --to [--mode ] [--allow-drift]
+```
+
+| Flag | Effect |
+| --------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
+| `--to ` (required) | Target profile name |
+| `--mode ` | `add`, `merge`, or `mirror`; changes what the preview plans for |
+| `--allow-drift` | Preview despite a Directus version or database vendor mismatch (see [the compatibility gate](#the-compatibility-gate)) |
+| `--project ` | Project to sync (default: `default`) |
+
+Applies nothing, and exits `0` whether or not differences exist; automation reads the report's `changes` field.
+
+## `d6s sync push`
+
+```bash
+d6s sync push --to [--mode ] [--yes] [--dangerously-allow-delete] [--allow-drift]
+```
+
+| Flag | Effect |
+| ---------------------------- | ------------------------------------------------------------------------------------------------------------------- |
+| `--to ` (required) | Target profile name |
+| `--mode ` | `add`, `merge` (default), or `mirror` |
+| `--yes` | Skip the apply confirmation; never authorizes deletions |
+| `--dangerously-allow-delete` | Consent to deletions; required for non-interactive `mirror` |
+| `--allow-drift` | Push despite a Directus version or database vendor mismatch (see [the compatibility gate](#the-compatibility-gate)) |
+| `--project ` | Project to sync (default: `default`) |
+
+## `directus.config.json`
+
+Created by `d6s profile add` and changed by `d6s profile update`; found by walking up from the current directory, like git finds `.git`. It never contains credentials, so commit it. This example uses include scopes; each axis can use an exclude scope instead:
+
+```json
+{
+ "profiles": {
+ "staging": {
+ "url": "https://staging.example.com",
+ "auth": { "type": "token" }
+ },
+ "production": {
+ "url": "https://cms.example.com",
+ "auth": { "type": "token" }
+ }
+ },
+ "directory": "directus",
+ "format": "json",
+ "projects": {
+ "default": {
+ "schema": true,
+ "collections": ["pages", "posts"],
+ "resources": ["flows", "settings"],
+ "mode": "merge"
+ }
+ }
+}
+```
+
+Top-level keys:
+
+| Key | Meaning | Default |
+| ----------- | --------------------------------------------------- | ------------ |
+| `profiles` | Named instances and their URLs | `{}` |
+| `directory` | The directory pulls write into and pushes read from | `"directus"` |
+| `format` | Sync-file format; currently only `"json"` | `"json"` |
+| `projects` | Per-project sync options (see below) | `{}` |
+
+`auth.type` is currently always `"token"`. The static token or saved login session itself stays outside this file.
+
+Per-project keys, all optional. A project is a named slice of the sync with its own subdirectory (`//`); the `default` project exists without being declared. Flags on the command line override these per run:
+
+| Key | Meaning |
+| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `schema` | `false` makes this a configuration-only project: pull, diff, and push never touch schema, and reports say `schemaSkipped` so automation can tell a skipped phase from a matching one |
+| `collections` | Schema scope: only these collections |
+| `excludeCollections` | Schema scope: all collections except these |
+| `resources` | Only these configuration resources |
+| `excludeResources` | The default resources except these |
+| `mode` | Default push mode for this project: `add`, `merge`, or `mirror` |
+| `deps` | `false` skips pulling selected resources' dependencies |
+
+`"schema": false` combined with a collections scope is refused as a contradiction, as is setting both an include and an exclude list for the same axis. Project names use letters, numbers, hyphens, and underscores.
+
+## Credentials
+
+When a command authenticates a profile, its credential resolves in order:
+
+1. A `--token` flag on `profile add`, `profile update`, or `profile test-connection`. The sync commands take no token flag.
+2. The `DIRECTUS__TOKEN` environment variable: the profile name uppercased, so `production` reads `DIRECTUS_PRODUCTION_TOKEN`. A `.env` file next to `directus.config.json` is loaded automatically without overriding real environment variables.
+3. The static token or login session in `~/.directus/credentials.json`, written readable only by you (mode `0600`). Never consulted when `CI` is non-empty.
+
+Use an admin credential. The Schema endpoints and the batch import the CLI relies on are admin-only on the server, and non-admin reads are silently permission-filtered — so the CLI verifies admin access at the start of every pull, diff, and push and refuses a non-admin token outright.
+
+## Configuration resources
+
+Resource selection follows a dependency graph: selecting a resource pulls in what it needs (unless `--no-deps`).
+
+| Resource | In default pull | Select directly | Also includes | Notes |
+| -------------- | ---------------------------- | ---------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `roles` | Yes | `--roles` | `policies` | |
+| `policies` | Yes | `--policies` | `access`, `permissions` | See the warning below before selecting policies on their own. |
+| `access` | Yes, with roles and policies | — | — | Grants attached to users are dropped when users are out of scope; a mirror push does not delete them on the target. |
+| `permissions` | Yes, with policies | — | — | Record counts are verified against the server. If the source hides records (unlicensed custom permission rules), the pull is incomplete. Stored duplicates of the built-in app-access permissions are dropped and reported; the server recreates them. |
+| `flows` | Yes | `--flows` | `operations` | |
+| `operations` | Yes, with flows | — | — | |
+| `dashboards` | Yes | `--dashboards` | `panels` | |
+| `panels` | Yes, with dashboards | — | — | Panels have no field to match on, so a first push into a matching target can duplicate once; the ID map prevents repeats. |
+| `settings` | Yes | `--settings` | — | A single record. License and AI credentials, branding images (logos, backgrounds, favicon), and the default storage folder are stripped. |
+| `folders` | Yes | `--folders` | — | The media-library folder tree. Distinct from collection folders (Data Studio sidebar groups), which sync as Schema. |
+| `translations` | Yes | `--translations` | — | Matched by `language` and `key`; on Directus 12.2.0 and later, `merge` and `mirror` can update existing strings. |
+| `users` | Opt-in | `--users` | `roles`, `policies` | Secret fields (`password`, `token`, `tfa_secret`, and others) are stripped. |
+
+::callout{icon="i-lucide-triangle-alert" color="warning"}
+**Select `--roles`, not `--policies` alone**
+A selection that pulls policies without roles is not independently pushable when access records reference roles. With no roles in scope, a push to a fresh target fails. Select `--roles` instead; it includes policies and their dependent resources too.
+::
+
+## What a pull touches
+
+Two rules govern every pull, scoped or not:
+
+1. A pull overwrites the sync files in its requested scope with the source's current state. Files outside that scope are left unchanged.
+2. A push only applies what is in the sync files. Work that never entered them cannot ship.
+
+**Overwritten from source** means the CLI replaced the files in scope with the current source state. Because output is deterministic, an identical source state produces byte-identical files and no git diff. **Left unchanged** means the pull did not touch those files.
+
+| Pull | Schema sync files | Configuration sync files |
+| ------------------------------------ | ----------------------------------------------------- | ----------------------------------------------------------------------- |
+| `pull --from staging` | All overwritten from source | Default set overwritten; users left unchanged |
+| `... --collections posts` | `posts` overwritten; all others left unchanged | Default set overwritten; users left unchanged |
+| `... --no-flows` | All overwritten from source | Other defaults overwritten; flows, operations, and users left unchanged |
+| `... --no-translations` | All overwritten from source | Other defaults overwritten; translations and users left unchanged |
+| `... --flows` | All overwritten (resource flags do not narrow Schema) | Flows and operations overwritten; all others left unchanged |
+| `... --flows --no-schema` | All left unchanged | Flows and operations overwritten; all others left unchanged |
+| `... --collections posts --no-flows` | `posts` overwritten; all others left unchanged | Other defaults overwritten; flows, operations, and users left unchanged |
+
+## Push modes
+
+| Mode | Schema | Configuration | Deletes? |
+| ----------------- | ---------------------------------- | ---------------------------------------------------------------- | -------------- |
+| `add` | Adds and modifies, same as `merge` | Creates only; existing records are never updated | No |
+| `merge` (default) | Adds and modifies | Creates and updates | No |
+| `mirror` | May delete in scope | Creates, updates, and deletes records absent from the sync files | **Yes, gated** |
+
+A `mirror` push deletes only within what the sync files cover: a pull scoped to some collections can delete fields inside those collections, never a collection it doesn't contain. A pull the source itself left incomplete (hidden permission records) is refused at mirror push outright.
+
+## Deletion gates
+
+Only `mirror` deletes, and deleting always requires its own explicit consent:
+
+| Context | To delete you must |
+| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
+| Interactive terminal | Review the plan naming the losses, then type the profile name (unless you passed `--dangerously-allow-delete`, which is the consent) |
+| Non-interactive / CI | Pass `--dangerously-allow-delete` |
+
+`--yes` skips the ordinary confirmation prompt, but it never authorizes a deletion; that holds even if a non-deleting push unexpectedly carries one. A `mirror` push in CI without `--dangerously-allow-delete` refuses before changing anything:
+
+```cli
+✖ Refusing mirror mode in a non-interactive context without --dangerously-allow-delete.
+ mirror can delete schema and configuration records absent from ./directus/default; pass --dangerously-allow-delete to consent, or use --mode merge.
+```
+
+## Record identity
+
+Records are matched across instances first by the ID map (`//id_map.json`), then by identifying fields:
+
+| Resource | Matched by |
+| ----------------------------------------------- | ------------------------------------ |
+| Roles, policies, flows, dashboards, and folders | `name` |
+| Users | `email` |
+| Access rules | `role`, `user`, and `policy` |
+| Permissions | `policy`, `collection`, and `action` |
+| Operations | `flow` and `key` |
+| Translations | `language` and `key` |
+| Settings | The single settings record |
+| Panels | Nothing; ID map only |
+
+The ID map is keyed internally by source and target instance URL, so one file serves any number of targets without conflicts. Commit it whenever a push changes it. An ambiguous match (two candidates) prompts interactively and refuses non-interactively:
+
+```cli
+✖ Push refused: 1 target match needs a choice.
+ directus_roles: ./directus/default contains 1 role named "Editor".
+ production — https://cms.example.com contains 2 matching roles.
+ Run d6s sync push interactively once to choose, then commit the updated ID map.
+```
+
+## The compatibility gate
+
+Environment Sync requires Directus 12.2.0 or later. Schema comparison requires the version recorded in the sync files and the target's version to match exactly, patch release included. The target server also requires the snapshot's database vendor to match its own. The CLI names both versions when it can detect a version mismatch; it cannot pre-check the target vendor, so it translates the server's refusal into an incompatible-snapshot error and keeps the server's reason as the detail.
+
+`--allow-drift` sends the server's `force` bypass for either mismatch and prints a `▲ Compatibility check bypassed` warning. It does not translate schema between Directus versions or database vendors. Projects with `"schema": false` skip the version-parity and vendor comparison; the 12.2.0 minimum still applies. When the target version cannot be read, the server makes the compatibility decision.
+
+## JSON reports
+
+With `--json`, stdout carries exactly one report per command; warnings still go to stderr so logs keep them while stdout stays parseable. Reports are emitted as a single line; they're formatted here for readability.
+
+`d6s sync pull --from staging --json`:
+
+```json
+{
+ "source": "https://staging.example.com",
+ "profile": "staging",
+ "project": "default",
+ "schemaSkipped": false,
+ "dir": "directus/default/schema",
+ "collections": 12,
+ "fields": 87,
+ "systemFields": 2,
+ "relations": 14,
+ "files": 13,
+ "removed": [],
+ "scope": null,
+ "data": {
+ "resources": [
+ "access",
+ "folders",
+ "operations",
+ "flows",
+ "panels",
+ "dashboards",
+ "permissions",
+ "policies",
+ "roles",
+ "settings",
+ "translations"
+ ],
+ "collections": [
+ "directus_access",
+ "directus_folders",
+ "directus_operations",
+ "directus_flows",
+ "directus_panels",
+ "directus_dashboards",
+ "directus_permissions",
+ "directus_policies",
+ "directus_roles",
+ "directus_settings",
+ "directus_translations"
+ ],
+ "recordCount": 57,
+ "collectionCount": 11,
+ "fileCount": 12,
+ "removed": [],
+ "incomplete": []
+ }
+}
+```
+
+The Schema counters (`collections` through `files`) are `null` when the Schema phase is skipped; `scope` echoes a `--collections`/`--exclude-collections` scope; `data.incomplete` names resources whose pull the source cut short. The Configuration report lives under `data`; the name is kept for backward compatibility.
+
+`d6s sync diff --to production --json`:
+
+```json
+{
+ "target": "https://cms.example.com",
+ "profile": "production",
+ "project": "default",
+ "mode": "merge",
+ "changes": true,
+ "schemaSkipped": false,
+ "added": 1,
+ "modified": 1,
+ "deleted": 0,
+ "hash": "8a265a26cce1",
+ "data": {
+ "mode": "merge",
+ "source": "https://staging.example.com",
+ "resultsByCollection": {
+ "directus_flows": {
+ "existing": [],
+ "new": ["f1"],
+ "deleted": [],
+ "mapped": {}
+ }
+ },
+ "reconciliation": {
+ "matched": 1,
+ "unmatched": 1,
+ "ambiguous": 0,
+ "dependent": 0
+ },
+ "unchanged": 0,
+ "incomplete": []
+ }
+}
+```
+
+`changes` is `true` when a push would do anything, including when `data.reconciliation.ambiguous` is non-zero. `added`/`modified`/`deleted` count Schema items. `data.resultsByCollection` is the target server's own per-collection dry-run answer. Reconciliation separates records that matched, have no match, need a choice, or depend on a choice; `unchanged` counts matched records whose synced values already agree.
+
+`d6s sync push --to production --yes --json` reports the same top-level shape and adds `applied`, which is `true` when the push changed the target. Its `data.resultsByCollection` reflects what the import actually did; `data.reconciliation` and `data.unchanged` are `null` because push reports applied results rather than diff-only comparison counts.
+
+Failures put an error report on stdout:
+
+```json
+{
+ "error": {
+ "code": "STATE",
+ "message": "Version mismatch: the snapshot was pulled from Directus 12.1.1, but the target runs 12.2.0.",
+ "hint": "..."
+ }
+}
+```
+
+The `code` is one of a small set of failure classes: `USAGE` (the command line needs fixing: a missing flag or missing consent), `UNKNOWN_COMMAND`, `CONFIG` (saved configuration is missing, invalid, or conflicts with the request), `AUTH` (the credential was rejected), `HTTP` (the instance could not be reached or returned an error), `STATE` (the sync files and the instance disagree: version mismatch, changed target Schema, incomplete pull), or `UNKNOWN`. Exit codes are `0` for success and `1` for every failure; the code string is the finer-grained signal.
+
+## Output conventions
+
+Human-readable status lines go to stderr, prefixed `●` (info), `◇` (success), `▲` (warning), or `✖` (error, with its hint indented beneath). Command results and plan lines go to stdout: `+` marks an addition, `~` a modification, and `✖ DELETE` a deletion, with Configuration plans summarized per collection as `+N new ~N updated ✖N deleted`. Legacy Windows consoles use the ASCII equivalents `i`, `+`, `!`, `x`, and `x DELETE`. `--no-color` disables coloring; `--json` replaces stdout output with the report while warnings stay on stderr.
diff --git a/content/guides/10.deployments/.navigation.yml b/content/guides/11.deployments/.navigation.yml
similarity index 100%
rename from content/guides/10.deployments/.navigation.yml
rename to content/guides/11.deployments/.navigation.yml
diff --git a/content/guides/10.deployments/0.index.md b/content/guides/11.deployments/0.index.md
similarity index 100%
rename from content/guides/10.deployments/0.index.md
rename to content/guides/11.deployments/0.index.md
diff --git a/content/guides/10.deployments/1.security.md b/content/guides/11.deployments/1.security.md
similarity index 100%
rename from content/guides/10.deployments/1.security.md
rename to content/guides/11.deployments/1.security.md
diff --git a/content/guides/11.ai/.navigation.yml b/content/guides/12.ai/.navigation.yml
similarity index 100%
rename from content/guides/11.ai/.navigation.yml
rename to content/guides/12.ai/.navigation.yml
diff --git a/content/guides/11.ai/0.index.md b/content/guides/12.ai/0.index.md
similarity index 100%
rename from content/guides/11.ai/0.index.md
rename to content/guides/12.ai/0.index.md
diff --git a/content/guides/11.ai/1.assistant/.navigation.yml b/content/guides/12.ai/1.assistant/.navigation.yml
similarity index 100%
rename from content/guides/11.ai/1.assistant/.navigation.yml
rename to content/guides/12.ai/1.assistant/.navigation.yml
diff --git a/content/guides/11.ai/1.assistant/0.index.md b/content/guides/12.ai/1.assistant/0.index.md
similarity index 100%
rename from content/guides/11.ai/1.assistant/0.index.md
rename to content/guides/12.ai/1.assistant/0.index.md
diff --git a/content/guides/11.ai/1.assistant/1.setup.md b/content/guides/12.ai/1.assistant/1.setup.md
similarity index 100%
rename from content/guides/11.ai/1.assistant/1.setup.md
rename to content/guides/12.ai/1.assistant/1.setup.md
diff --git a/content/guides/11.ai/1.assistant/2.usage.md b/content/guides/12.ai/1.assistant/2.usage.md
similarity index 100%
rename from content/guides/11.ai/1.assistant/2.usage.md
rename to content/guides/12.ai/1.assistant/2.usage.md
diff --git a/content/guides/11.ai/1.assistant/3.tools.md b/content/guides/12.ai/1.assistant/3.tools.md
similarity index 100%
rename from content/guides/11.ai/1.assistant/3.tools.md
rename to content/guides/12.ai/1.assistant/3.tools.md
diff --git a/content/guides/11.ai/1.assistant/4.tips.md b/content/guides/12.ai/1.assistant/4.tips.md
similarity index 100%
rename from content/guides/11.ai/1.assistant/4.tips.md
rename to content/guides/12.ai/1.assistant/4.tips.md
diff --git a/content/guides/11.ai/1.assistant/5.security.md b/content/guides/12.ai/1.assistant/5.security.md
similarity index 100%
rename from content/guides/11.ai/1.assistant/5.security.md
rename to content/guides/12.ai/1.assistant/5.security.md
diff --git a/content/guides/11.ai/2.mcp/.navigation.yml b/content/guides/12.ai/2.mcp/.navigation.yml
similarity index 100%
rename from content/guides/11.ai/2.mcp/.navigation.yml
rename to content/guides/12.ai/2.mcp/.navigation.yml
diff --git a/content/guides/11.ai/2.mcp/0.index.md b/content/guides/12.ai/2.mcp/0.index.md
similarity index 100%
rename from content/guides/11.ai/2.mcp/0.index.md
rename to content/guides/12.ai/2.mcp/0.index.md
diff --git a/content/guides/11.ai/2.mcp/1.installation.md b/content/guides/12.ai/2.mcp/1.installation.md
similarity index 100%
rename from content/guides/11.ai/2.mcp/1.installation.md
rename to content/guides/12.ai/2.mcp/1.installation.md
diff --git a/content/guides/11.ai/2.mcp/2.use-cases.md b/content/guides/12.ai/2.mcp/2.use-cases.md
similarity index 100%
rename from content/guides/11.ai/2.mcp/2.use-cases.md
rename to content/guides/12.ai/2.mcp/2.use-cases.md
diff --git a/content/guides/11.ai/2.mcp/3.tools.md b/content/guides/12.ai/2.mcp/3.tools.md
similarity index 100%
rename from content/guides/11.ai/2.mcp/3.tools.md
rename to content/guides/12.ai/2.mcp/3.tools.md
diff --git a/content/guides/11.ai/2.mcp/4.prompts.md b/content/guides/12.ai/2.mcp/4.prompts.md
similarity index 100%
rename from content/guides/11.ai/2.mcp/4.prompts.md
rename to content/guides/12.ai/2.mcp/4.prompts.md
diff --git a/content/guides/11.ai/2.mcp/5.troubleshooting.md b/content/guides/12.ai/2.mcp/5.troubleshooting.md
similarity index 100%
rename from content/guides/11.ai/2.mcp/5.troubleshooting.md
rename to content/guides/12.ai/2.mcp/5.troubleshooting.md
diff --git a/content/guides/11.ai/2.mcp/6.oauth.md b/content/guides/12.ai/2.mcp/6.oauth.md
similarity index 100%
rename from content/guides/11.ai/2.mcp/6.oauth.md
rename to content/guides/12.ai/2.mcp/6.oauth.md
diff --git a/content/guides/11.ai/2.mcp/7.security.md b/content/guides/12.ai/2.mcp/7.security.md
similarity index 100%
rename from content/guides/11.ai/2.mcp/7.security.md
rename to content/guides/12.ai/2.mcp/7.security.md
diff --git a/content/guides/11.ai/2.mcp/8.local-mcp/.navigation.yml b/content/guides/12.ai/2.mcp/8.local-mcp/.navigation.yml
similarity index 100%
rename from content/guides/11.ai/2.mcp/8.local-mcp/.navigation.yml
rename to content/guides/12.ai/2.mcp/8.local-mcp/.navigation.yml
diff --git a/content/guides/11.ai/2.mcp/8.local-mcp/0.index.md b/content/guides/12.ai/2.mcp/8.local-mcp/0.index.md
similarity index 100%
rename from content/guides/11.ai/2.mcp/8.local-mcp/0.index.md
rename to content/guides/12.ai/2.mcp/8.local-mcp/0.index.md
diff --git a/content/guides/11.ai/2.mcp/8.local-mcp/1.tools.md b/content/guides/12.ai/2.mcp/8.local-mcp/1.tools.md
similarity index 100%
rename from content/guides/11.ai/2.mcp/8.local-mcp/1.tools.md
rename to content/guides/12.ai/2.mcp/8.local-mcp/1.tools.md
diff --git a/content/guides/11.ai/2.mcp/8.local-mcp/2.prompts.md b/content/guides/12.ai/2.mcp/8.local-mcp/2.prompts.md
similarity index 100%
rename from content/guides/11.ai/2.mcp/8.local-mcp/2.prompts.md
rename to content/guides/12.ai/2.mcp/8.local-mcp/2.prompts.md
diff --git a/content/guides/11.ai/3.translations.md b/content/guides/12.ai/3.translations.md
similarity index 100%
rename from content/guides/11.ai/3.translations.md
rename to content/guides/12.ai/3.translations.md
diff --git a/content/guides/12.integrations/.navigation.yml b/content/guides/13.integrations/.navigation.yml
similarity index 100%
rename from content/guides/12.integrations/.navigation.yml
rename to content/guides/13.integrations/.navigation.yml
diff --git a/content/guides/12.integrations/1.n8n/.navigation.yml b/content/guides/13.integrations/1.n8n/.navigation.yml
similarity index 100%
rename from content/guides/12.integrations/1.n8n/.navigation.yml
rename to content/guides/13.integrations/1.n8n/.navigation.yml
diff --git a/content/guides/12.integrations/1.n8n/0.index.md b/content/guides/13.integrations/1.n8n/0.index.md
similarity index 100%
rename from content/guides/12.integrations/1.n8n/0.index.md
rename to content/guides/13.integrations/1.n8n/0.index.md
diff --git a/content/guides/12.integrations/1.n8n/directus-n8n-actions.md b/content/guides/13.integrations/1.n8n/directus-n8n-actions.md
similarity index 100%
rename from content/guides/12.integrations/1.n8n/directus-n8n-actions.md
rename to content/guides/13.integrations/1.n8n/directus-n8n-actions.md
diff --git a/content/guides/12.integrations/1.n8n/directus-n8n-advanced.md b/content/guides/13.integrations/1.n8n/directus-n8n-advanced.md
similarity index 100%
rename from content/guides/12.integrations/1.n8n/directus-n8n-advanced.md
rename to content/guides/13.integrations/1.n8n/directus-n8n-advanced.md
diff --git a/content/guides/12.integrations/1.n8n/directus-n8n-triggers.md b/content/guides/13.integrations/1.n8n/directus-n8n-triggers.md
similarity index 100%
rename from content/guides/12.integrations/1.n8n/directus-n8n-triggers.md
rename to content/guides/13.integrations/1.n8n/directus-n8n-triggers.md
diff --git a/content/guides/12.integrations/2.clay/.navigation.yml b/content/guides/13.integrations/2.clay/.navigation.yml
similarity index 100%
rename from content/guides/12.integrations/2.clay/.navigation.yml
rename to content/guides/13.integrations/2.clay/.navigation.yml
diff --git a/content/guides/12.integrations/2.clay/0.index.md b/content/guides/13.integrations/2.clay/0.index.md
similarity index 100%
rename from content/guides/12.integrations/2.clay/0.index.md
rename to content/guides/13.integrations/2.clay/0.index.md
diff --git a/content/guides/12.integrations/2.clay/directus-clay-data-operations.md b/content/guides/13.integrations/2.clay/directus-clay-data-operations.md
similarity index 100%
rename from content/guides/12.integrations/2.clay/directus-clay-data-operations.md
rename to content/guides/13.integrations/2.clay/directus-clay-data-operations.md
diff --git a/content/guides/12.integrations/2.clay/use-clay-templates-with-directus.md b/content/guides/13.integrations/2.clay/use-clay-templates-with-directus.md
similarity index 100%
rename from content/guides/12.integrations/2.clay/use-clay-templates-with-directus.md
rename to content/guides/13.integrations/2.clay/use-clay-templates-with-directus.md
diff --git a/content/guides/12.integrations/2.clay/use-directus-webhooks-with-clay.md b/content/guides/13.integrations/2.clay/use-directus-webhooks-with-clay.md
similarity index 100%
rename from content/guides/12.integrations/2.clay/use-directus-webhooks-with-clay.md
rename to content/guides/13.integrations/2.clay/use-directus-webhooks-with-clay.md
diff --git a/content/guides/12.integrations/3.zapier/.navigation.yml b/content/guides/13.integrations/3.zapier/.navigation.yml
similarity index 100%
rename from content/guides/12.integrations/3.zapier/.navigation.yml
rename to content/guides/13.integrations/3.zapier/.navigation.yml
diff --git a/content/guides/12.integrations/3.zapier/0.index.md b/content/guides/13.integrations/3.zapier/0.index.md
similarity index 100%
rename from content/guides/12.integrations/3.zapier/0.index.md
rename to content/guides/13.integrations/3.zapier/0.index.md
diff --git a/content/guides/12.integrations/3.zapier/actions.md b/content/guides/13.integrations/3.zapier/actions.md
similarity index 100%
rename from content/guides/12.integrations/3.zapier/actions.md
rename to content/guides/13.integrations/3.zapier/actions.md
diff --git a/content/guides/12.integrations/3.zapier/advanced.md b/content/guides/13.integrations/3.zapier/advanced.md
similarity index 100%
rename from content/guides/12.integrations/3.zapier/advanced.md
rename to content/guides/13.integrations/3.zapier/advanced.md
diff --git a/content/guides/12.integrations/3.zapier/triggers.md b/content/guides/13.integrations/3.zapier/triggers.md
similarity index 100%
rename from content/guides/12.integrations/3.zapier/triggers.md
rename to content/guides/13.integrations/3.zapier/triggers.md
diff --git a/content/guides/12.integrations/4.vercel/.navigation.yml b/content/guides/13.integrations/4.vercel/.navigation.yml
similarity index 100%
rename from content/guides/12.integrations/4.vercel/.navigation.yml
rename to content/guides/13.integrations/4.vercel/.navigation.yml
diff --git a/content/guides/12.integrations/4.vercel/0.index.md b/content/guides/13.integrations/4.vercel/0.index.md
similarity index 100%
rename from content/guides/12.integrations/4.vercel/0.index.md
rename to content/guides/13.integrations/4.vercel/0.index.md
diff --git a/content/guides/12.integrations/4.vercel/deployments.md b/content/guides/13.integrations/4.vercel/deployments.md
similarity index 100%
rename from content/guides/12.integrations/4.vercel/deployments.md
rename to content/guides/13.integrations/4.vercel/deployments.md
diff --git a/content/guides/12.integrations/5.netlify/.navigation.yml b/content/guides/13.integrations/5.netlify/.navigation.yml
similarity index 100%
rename from content/guides/12.integrations/5.netlify/.navigation.yml
rename to content/guides/13.integrations/5.netlify/.navigation.yml
diff --git a/content/guides/12.integrations/5.netlify/0.index.md b/content/guides/13.integrations/5.netlify/0.index.md
similarity index 100%
rename from content/guides/12.integrations/5.netlify/0.index.md
rename to content/guides/13.integrations/5.netlify/0.index.md
diff --git a/content/guides/12.integrations/5.netlify/deployments.md b/content/guides/13.integrations/5.netlify/deployments.md
similarity index 100%
rename from content/guides/12.integrations/5.netlify/deployments.md
rename to content/guides/13.integrations/5.netlify/deployments.md
diff --git a/content/guides/12.integrations/6.airbyte/0.index.md b/content/guides/13.integrations/6.airbyte/0.index.md
similarity index 100%
rename from content/guides/12.integrations/6.airbyte/0.index.md
rename to content/guides/13.integrations/6.airbyte/0.index.md
diff --git a/content/guides/12.integrations/6.framer/.navigation.yml b/content/guides/13.integrations/6.framer/.navigation.yml
similarity index 100%
rename from content/guides/12.integrations/6.framer/.navigation.yml
rename to content/guides/13.integrations/6.framer/.navigation.yml
diff --git a/content/guides/12.integrations/6.framer/0.index.md b/content/guides/13.integrations/6.framer/0.index.md
similarity index 100%
rename from content/guides/12.integrations/6.framer/0.index.md
rename to content/guides/13.integrations/6.framer/0.index.md
diff --git a/content/guides/12.integrations/6.supabase/.navigation.yml b/content/guides/13.integrations/6.supabase/.navigation.yml
similarity index 100%
rename from content/guides/12.integrations/6.supabase/.navigation.yml
rename to content/guides/13.integrations/6.supabase/.navigation.yml
diff --git a/content/guides/12.integrations/6.supabase/0.index.md b/content/guides/13.integrations/6.supabase/0.index.md
similarity index 100%
rename from content/guides/12.integrations/6.supabase/0.index.md
rename to content/guides/13.integrations/6.supabase/0.index.md
diff --git a/content/guides/12.integrations/6.supabase/connect-supabase-postgres-to-directus.md b/content/guides/13.integrations/6.supabase/connect-supabase-postgres-to-directus.md
similarity index 100%
rename from content/guides/12.integrations/6.supabase/connect-supabase-postgres-to-directus.md
rename to content/guides/13.integrations/6.supabase/connect-supabase-postgres-to-directus.md
diff --git a/content/guides/12.integrations/6.supabase/use-supabase-storage-with-directus.md b/content/guides/13.integrations/6.supabase/use-supabase-storage-with-directus.md
similarity index 100%
rename from content/guides/12.integrations/6.supabase/use-supabase-storage-with-directus.md
rename to content/guides/13.integrations/6.supabase/use-supabase-storage-with-directus.md
diff --git a/content/guides/12.integrations/index.md b/content/guides/13.integrations/index.md
similarity index 100%
rename from content/guides/12.integrations/index.md
rename to content/guides/13.integrations/index.md
diff --git a/content/guides/13.security/1.best-practices.md b/content/guides/14.security/1.best-practices.md
similarity index 100%
rename from content/guides/13.security/1.best-practices.md
rename to content/guides/14.security/1.best-practices.md