Promoting Changes Between Environments
In BuildPad DaaS, different environments (Development, Staging, and Production) are managed as separate project instances. This guide explains how to safely migrate and promote data model changes between these environments using the Schema Migration API.
All Schema Migration endpoints require Admin privileges (isAdmin). Always verify changes in a staging environment before applying them to production.
The Migration Lifecycle
The promotion workflow consists of three main steps:
[Development (Base)] [Staging / Production (Target)]
│ │
├──────── 1. GET /api/schema/snapshot ───────────────────►│
│ Capture data model snapshot │
│ │
├──────── 2. POST /api/schema/diff ──────────────────────►│
│ Calculate drift & target schema hash │
│ │
├──────── 3. POST /api/schema/apply ─────────────────────►│
│ Validate hash, apply DDL & update metadata │
│ Returns 204 No Content │- Snapshot (
GET /api/schema/snapshot): Export the collection, field, and relation metadata from the base (source/dev) environment. - Diff (
POST /api/schema/diff): Compare the snapshot against the target environment’s live schema, generating the exact delta alongside a target schema safetyhash. - Apply (
POST /api/schema/apply): Apply the structured diff to the target environment. DDL, row-level security for new tables, and system metadata run in a single transaction: every change lands, or none does.
Step-by-Step Tutorial
Retrieve Data Model Snapshot from Base (Dev)
Perform a GET request to /api/schema/snapshot on your development environment:
GET /api/schema/snapshot
Authorization: Bearer <DEV_ADMIN_TOKEN>To promote only specific collections, pass the includeCollections query parameter:
GET /api/schema/snapshot?includeCollections=articles,authors,categories
Authorization: Bearer <DEV_ADMIN_TOKEN>Snapshots scoped with includeCollections or excludeCollections produce version: 2. A diff of a scoped snapshot never deletes collections, and even with mode=mirror it only deletes fields and relations inside the collections the snapshot contains. Collections outside the snapshot are left untouched.
Retrieve Data Model Diff on Target (Staging/Prod)
Send the JSON snapshot captured in Step 1 to the target environment’s /api/schema/diff endpoint:
POST /api/schema/diff?mode=merge
Authorization: Bearer <TARGET_ADMIN_TOKEN>
Content-Type: application/json
{
"version": 1,
"collections": [ ... ],
"fields": [ ... ],
"relations": [ ... ]
}Migration Modes:
| Mode | Parameter | Description |
|---|---|---|
| Merge (Default) | mode=merge | Additive: only creates and updates. Extra collections, fields and relations on the target are kept. |
| Mirror | mode=mirror | Also marks collections, fields and relations that exist on the target but not in the snapshot for deletion ("kind": "D"). Deleting a collection drops its table and data. |
Always review a mirror diff before applying it. Every "kind": "D" entry is permanent once applied.
Some parts of the schema are never included in a diff:
- System collections (
daas_*,directus_*,xtr_*), which each instance’s own migrations manage. - Database views.
- Scope fields (foreign keys to
daas_scope_items). These are never deleted, because scope configuration is per environment. - Metadata that is missing from the snapshot. It is treated as “no change”, not as a deletion.
The endpoint returns a diff payload containing a cryptographic hash:
{
"data": {
"hash": "8f3b20755919e17b8893d39572b9a7852c0049281729da842b1fe186e80b2713",
"collections": [
{
"collection": "articles",
"diff": { "kind": "N", "rhs": { "collection": "articles", ... } }
}
],
"fields": [ ... ],
"relations": [ ... ]
}
}Apply Diff to Target Environment
Send the exact diff output from Step 2 to /api/schema/apply on the target project:
POST /api/schema/apply
Authorization: Bearer <TARGET_ADMIN_TOKEN>
Content-Type: application/json
{
"hash": "8f3b20755919e17b8893d39572b9a7852c0049281729da842b1fe186e80b2713",
"collections": [ ... ],
"fields": [ ... ],
"relations": [ ... ]
}A response status of 204 No Content indicates that the schema changes and internal metadata (daas_collections, daas_fields, daas_relations) were applied successfully. New tables get the same row-level security policies as tables created with POST /api/collections.
| Status | Meaning |
|---|---|
204 | Every change was applied. |
400 | The payload is invalid (for example, hash is missing), or the diff asks for an unsupported change. Nothing was changed. |
409 (SCHEMA_MODIFIED) | The target schema changed after the diff was generated. Generate a new diff and apply again. |
500 | The database rejected a statement (for example, adding a NOT NULL column without a default to a table that has rows). The whole migration was rolled back. |
The following changes are not supported and return 400:
- Adding, removing or changing a primary key, or changing auto-increment.
- Converting between an alias field (O2M, M2M) and a database column.
- Column types that cannot be recreated from introspection, such as enums and other user-defined types, and arrays.
Unique constraint and index changes on existing columns are not migrated.
Code Examples
Node.js Script
import axios from 'axios';
const DEV_URL = 'https://dev-daas.example.com';
const PROD_URL = 'https://prod-daas.example.com';
const DEV_TOKEN = process.env.DEV_ADMIN_TOKEN;
const PROD_TOKEN = process.env.PROD_ADMIN_TOKEN;
async function runMigration() {
console.log('1. Exporting snapshot from Dev...');
const { data: snapshotRes } = await axios.get(`${DEV_URL}/api/schema/snapshot`, {
headers: { Authorization: `Bearer ${DEV_TOKEN}` },
params: {
// includeCollections: 'articles,authors' // Optional scoping
}
});
const snapshot = snapshotRes.data;
console.log('2. Generating schema diff on Prod...');
const { data: diffRes } = await axios.post(`${PROD_URL}/api/schema/diff`, snapshot, {
headers: {
Authorization: `Bearer ${PROD_TOKEN}`,
'Content-Type': 'application/json',
},
params: { mode: 'merge' }
});
const diff = diffRes.data;
console.log(`Diff generated with safety hash: ${diff.hash}`);
console.log('3. Applying schema diff to Prod...');
const applyRes = await axios.post(`${PROD_URL}/api/schema/apply`, diff, {
headers: {
Authorization: `Bearer ${PROD_TOKEN}`,
'Content-Type': 'application/json',
}
});
if (applyRes.status === 204) {
console.log('🎉 Schema changes successfully promoted!');
}
}
runMigration().catch(console.error);