Promoting Changes Between Environments in Directus
In Directus, different environments (development, staging, production) are managed as separate project instances. This guide explains how to safely promote changes between these environments.
Schema Changes
Schema changes should originate in your development environment. Use the Schema API to promote these changes to other environments. The API provides endpoints for taking snapshots, comparing schemas, and applying changes.
Content Management
Manage all production content as your single source of truth using:
- Status fields (draft, published, etc.)
- Roles and permissions
- Flows to control publishing process and procedures
Migration Options
When you need to migrate content as part of schema updates, you have several options:
- Data Studio: Use the built-in interface to export/import data in CSV, JSON, or XML formats.
- Import/Export API: Automate migrations using the Import and Export endpoints.
- Advanced Options:
- Custom extensions migrations
- Direct database operations (being careful with system tables)
- Using and modifying the template CLI to extract and load of all schema, system collections and content.
Migrate Your Schema
Directus' schema migration endpoints allow users to retrieve a project's data model and apply changes to another project.
This is useful if you make changes to a data model in a development project and need to apply them to a production project, or to move from a self-hosted project to Directus Cloud.
How-To Guide
You must be an admin user to use these endpoints and follow this guide.
You should have two Directus projects - this guide will refer to them as the "base" and the "target".
Set Up Project
Open a new empty directory in your code editor. In your terminal, navigate to the directory and install dependencies
with npm install @directus/sdk.
Create a new index.js file and set it up:
import { createDirectus, authentication, rest, schemaSnapshot, schemaDiff, schemaApply } from '@directus/sdk';
const BASE_DIRECTUS_URL = 'https://your-base-project.directus.app';
const TARGET_DIRECTUS_URL = 'https://your-target-project.directus.app';
const baseDirectus = createDirectus(BASE_DIRECTUS_URL).with(rest()).with(authentication());
const targetDirectus = createDirectus(TARGET_DIRECTUS_URL).with(rest()).with(authentication());;
await baseDirectus.login({ email: 'base_email', password: 'base_password' });
await targetDirectus.login({ email: 'target_email', password: 'target_password' });
async function main() {}
main();
Retrieve Data Model Snapshot From Base Project
At the bottom of index.js, create a getSnapshot() function:
function getSnapshot() {
return baseDirectus.request(schemaSnapshot());
}
This returns a snapshot of every collection in the project. To promote only part of your data model, scope the
snapshot with includeCollections or excludeCollections:
function getSnapshot() {
return baseDirectus.request(schemaSnapshot({ includeCollections: ['articles', 'authors'] }));
}
The two options are mutually exclusive, and collection names that do not exist in the base project are ignored. A
scoped snapshot is a partial snapshot, marked with version: 2 instead of version: 1. The diff step reads that
marker and limits itself to the collections the snapshot contains, so applying a partial snapshot never removes
collections that were left out of it.
In the main() function, call getSnapshot():
async function main() {
const snapshot = await getSnapshot(); console.log(snapshot); }
Get your snapshot by running node index.js.
Retrieve Data Model Diff
This section will create a "diff" that describes all differences between your base and target project's data models.
At the bottom of index.js, create a getDiff() function which accepts a snapshot parameter:
function getDiff(snapshot) {
return targetDirectus.request(schemaDiff(snapshot));
}
Update your main() function:
async function main() {
const snapshot = await getSnapshot();
console.log(snapshot); const diff = await getDiff(snapshot); console.log(diff); }
Get your diff by running node index.js.
By default the diff mirrors the base project: anything the target has that the base does not is marked for
deletion. Pass mode: 'merge' to get an additive diff instead, which omits operations that would delete a
collection, field, or relation:
function getDiff(snapshot) {
return targetDirectus.request(schemaDiff(snapshot, { mode: 'merge' }));
}
Use merge when the target project holds collections the base project should not remove, such as a production
environment with its own reporting tables. Use the default mirror mode when the base project is the single source
of truth for the whole data model.
Apply Diff To Target Project
At the bottom of index.js, create a applyDiff() function which accepts a diff parameter:
function applyDiff(diff) {
return targetDirectus.request(schemaApply(diff));
}
Update your main() function:
async function main() {
const snapshot = await getSnapshot();
const diff = await getDiff(snapshot);
console.log(diff); await applyDiff(diff); }
Apply the diff by running node index.js.
Handling Different Directus Versions
The diff endpoint does not allow different Directus versions and database vendors by default. This is to avoid any
unintentional diffs from being generated. You can opt in to bypass these checks by passing force: true alongside
the other diff options:
function getDiff(snapshot) {
return targetDirectus.request(schemaDiff(snapshot, { force: true }));
}
The hash property in the diff is based on the target instance's schema and version. It is used to safeguard against changes that may happen after the current diff was generated which can potentially incur unexpected side effects when applying the diffs without this safeguard. In case the schema has been changed in the meantime, the diff must be regenerated.
The complete and final code is available below.
import { createDirectus, authentication, rest, schemaSnapshot, schemaDiff, schemaApply } from '@directus/sdk';
const BASE_DIRECTUS_URL = 'https://your-base-project.directus.app';
const TARGET_DIRECTUS_URL = 'https://your-target-project.directus.app';
const baseDirectus = createDirectus(BASE_DIRECTUS_URL).with(rest()).with(authentication());;
const targetDirectus = createDirectus(TARGET_DIRECTUS_URL).with(rest()).with(authentication());;
await baseDirectus.login({ email: 'base_email', password: 'base_password' });
await targetDirectus.login({ email: 'target_email', password: 'target_password' });
async function main() {
const snapshot = await getSnapshot();
const diff = await getDiff(snapshot);
await applyDiff(diff);
}
main();
function getSnapshot() {
return baseDirectus.request(schemaSnapshot());
}
function getDiff(snapshot) {
return targetDirectus.request(schemaDiff(snapshot));
}
function applyDiff(diff) {
return targetDirectus.request(schemaApply(diff));
}
Retrieve Data Model Snapshot From Source Project
Perform a GET request to /schema/snapshot?access_token=<YOUR_ACCESS_TOKEN>.
Copy the JSON response with your data model snapshot.
To promote only part of your data model, scope the snapshot with either includeCollections or excludeCollections, passing a comma-separated list of collection names. The two parameters cannot be used together.
GET /schema/snapshot?includeCollections=articles,authors
A scoped snapshot is a partial snapshot, marked with version: 2 instead of version: 1. The diff step reads that marker and limits itself to the collections the snapshot contains, so applying a partial snapshot never removes collections that were left out of it.
Retrieve Data Model Diff
This section will create a "diff" that describes all differences between your source and target project's data models.
Perform a POST request to /schema/diff?access_token=<YOUR_ACCESS_TOKEN>, with the "Content Type" header set to application/json and the body set to the contents of the data property of JSON response from the snapshot.
Copy the JSON response with your data model diff.
By default the diff mirrors the source project, so anything the target has that the source does not is marked for deletion. Add mode=merge for an additive diff, which omits operations that would delete a collection, field, or relation. The default is mode=mirror.
POST /schema/diff?mode=merge
Apply Diff To Target Project
Perform a POST request to /schema/apply?access_token=<YOUR_ACCESS_TOKEN>, with the "Content Type" header set to application/json and the body set to the contents of the data property of the JSON response from the diff.
Note the response status of 204, which indicates a successful data model migration.
Final Tips
The diff endpoint does not allow different Directus versions and database vendors by default. This is to avoid any
unintentional diffs from being generated. You can opt in to bypass these checks by adding a second query parameter
called force with the value of true.
The hash property in the diff is based on the target instance's schema and version. It is used to safeguard against changes that may happen after the current diff was generated which can potentially incur unexpected side effects when applying the diffs without this safeguard. In case the schema has been changed in the meantime, the diff must be regenerated.