Quickstart

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:

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
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.

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

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:

d6s profile add source --url http://localhost:8055 --token source-token
◇ Saved profile "source" → http://localhost:8055
◇ Saved a token for "source" to the credential store.
d6s profile add target --url http://localhost:8056 --token target-token

That first command created directus.config.json in the current directory:

{
  "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:

d6s profile test source
◇ 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:

git init
d6s sync pull --from source
◇ 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/. 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:

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:

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.

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:

d6s sync diff --to target
● 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

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:

● 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 explains why.

Push again

d6s sync push --to target
● 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

docker compose down

Both instances and their databases are gone.

Where to go next

  • How It Works: the mental model behind what you just did, and the safety rules around deletions and record identity.
  • Common Workflows: the same loop applied to real situations, including adopting sync on an existing project.
  • Reference: every flag the commands take, including how to scope a pull to specific collections or resources.

Get once-a-month release notes & real‑world code tips...no fluff. 🐰