Schema API
The Schema API lets you read and modify the database schema at runtime. It covers collections (tables/views), fields (columns), relations (foreign keys), interfaces, and schema migration tools (snapshot, diff, apply).
All schema endpoints — including the read-only GET /api/schema/snapshot — are restricted to users with admin access. Schema changes run DDL statements directly on the connected PostgreSQL database.
Endpoint Summary
| Method | Path | Status | Admin | Description |
|---|---|---|---|---|
GET | /api/collections | 200 | No | List all collections |
GET | /api/collections/:name | 200 | No | Get collection metadata |
GET | /api/collections/:name/fields | 200 | No | List raw columns for a collection |
POST | /api/collections | 201 | Yes | Create a table or virtual folder |
PATCH | /api/collections | 200 | Yes | Batch update collection metadata |
PATCH | /api/collections/:name | 200 | Yes | Update collection metadata |
DELETE | /api/collections/:name | 204 | Yes | Drop table |
GET | /api/fields | 200 | No | List all fields |
GET | /api/fields/:collection | 200 | No | List fields for a collection |
GET | /api/fields/:collection/:field | 200 | No | Get single field metadata |
POST | /api/fields/:collection | 201 | Yes | Add a field |
PATCH | /api/fields/:collection/:field | 200 | Yes | Update a field |
DELETE | /api/fields/:collection/:field | 204 | Yes | Remove a field |
GET | /api/relations | 200 | No | List all relations |
GET | /api/relations/:collection | 200 | No | List relations for a collection |
GET | /api/relations/:collection/:field | 200 | No | Get single relation |
POST | /api/relations | 201 | Yes | Create a relation |
PATCH | /api/relations/:collection/:field | 200 | Yes | Update a relation |
DELETE | /api/relations/:collection/:field | 204 | Yes | Delete a relation |
GET | /api/interfaces | 200 | No | List available field interfaces |
GET | /api/schema/snapshot | 200 | Yes | Export schema snapshot |
POST | /api/schema/diff | 200 | Yes | Diff a snapshot against the live schema |
POST | /api/schema/apply | 204 | Yes | Apply a diff in one transaction |
Common error codes:
| Status | Meaning |
|---|---|
| 201 | Created |
| 204 | Deleted (no content) |
| 400 | Bad request (missing fields, invalid payload, duplicate, cannot delete primary key) |
| 401 | Unauthenticated |
| 403 | Forbidden (admin-only endpoint) |
| 404 | Not found |
| 409 | Conflict (duplicate collection/resources) |
Collections
Get all collections
curl https://your-domain.com/api/collections \
-H "Authorization: Bearer <token>"{
"data": [
{
"collection": "posts",
"meta": { "icon": "article", "note": "Blog posts", "sort_field": "sort" },
"schema": { "name": "posts", "schema": "public" },
"system": false,
"type": "table"
},
{
"collection": "daas_users",
"schema": { "name": "daas_users", "schema": "public" },
"meta": null,
"system": true,
"type": "table"
}
]
}| Field | Type | Description |
|---|---|---|
collection | string | Collection/table name |
schema | object | Schema info — name (table name) and schema (PostgreSQL schema, typically "public") |
meta | object | null | Collection metadata (icon, note, sort_field, etc.) |
system | boolean | Whether the collection is a system collection |
type | string | Collection type — "table" or "view" |
Get a single collection
GET /api/collections/:nameReturns a single collection object in the same shape as the list response.
Create a collection
curl -X POST https://your-domain.com/api/collections \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"collection": "products",
"meta": { "icon": "shopping_cart" },
"schema": {},
"fields": [
{ "field": "id", "type": "integer", "schema": { "is_primary_key": true, "has_auto_increment": true } },
{ "field": "name", "type": "string", "schema": { "is_nullable": false } },
{ "field": "price", "type": "float", "schema": { "is_nullable": true } }
]
}'Returns 201 Created with the collection object:
{
"data": {
"collection": "products",
"schema": { "name": "products", "schema": "public" },
"meta": { "icon": "shopping_cart" },
"system": false,
"type": "table"
}
}Virtual folders
Pass "schema": null to create a virtual folder — a collection with no physical database table and no required fields. Useful for organizing other collections into groups:
curl -X POST https://your-domain.com/api/collections \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"collection": "Blog Content",
"schema": null,
"meta": { "icon": "folder" }
}'Batch update collections
PATCH /api/collections updates metadata (sort order, grouping) for multiple collections at once. The body must be an array:
curl -X PATCH https://your-domain.com/api/collections \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '[
{ "collection": "articles", "meta": { "sort": 1, "group": "content" } },
{ "collection": "pages", "meta": { "sort": 2, "group": "content" } }
]'Returns 200 with the updated collection objects.
Update a single collection
PATCH /api/collections/:nameUpdates metadata for a single collection. Response is 200 with the updated collection object.
Delete a collection
DELETE /api/collections/:nameDrops the table and returns 204 No Content.
Fields
All fields endpoints support ?limit=N to cap the number of returned items.
Get all fields
GET /api/fields
GET /api/fields?limit=10Returns all field definitions across all collections in daas-compatible format:
{
"data": [
{
"collection": "posts",
"field": "title",
"type": "string",
"schema": {
"name": "title",
"table": "posts",
"data_type": "character varying",
"default_value": null,
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": false,
"is_unique": false,
"is_primary_key": false,
"has_auto_increment": false,
"has_index": false,
"foreign_key_table": null,
"foreign_key_column": null,
"comment": null
},
"meta": {
"collection": "posts",
"field": "title",
"interface": "input",
"required": true,
"hidden": false,
"readonly": false,
"sort": null,
"width": "full"
}
}
]
}Get fields for a collection
GET /api/fields/posts
GET /api/fields/posts?limit=5Same response format as above, filtered to a single collection.
Get a single field
GET /api/fields/posts/titleReturns a single field object.
Add a field
curl -X POST https://your-domain.com/api/fields/posts \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"field": "summary",
"type": "text",
"meta": { "interface": "input-multiline", "note": "Short description" },
"schema": { "is_nullable": true, "default_value": null }
}'Returns 201 Created with the new field object.
Update a field
PATCH /api/fields/posts/summaryUpdates field metadata and returns 200 with the updated field.
Delete a field
DELETE /api/fields/posts/summaryReturns 204 No Content on success.
Primary key columns cannot be deleted. Attempting to delete a primary key field returns a 400 error: Cannot delete primary key.
Raw column listing
GET /api/collections/:name/fields returns raw column information directly from PostgreSQL, in a different format than the daas-style /api/fields/:collection endpoint:
curl https://your-domain.com/api/collections/posts/fields \
-H "Authorization: Bearer <token>"{
"data": [
{
"field": "id",
"name": "Id",
"type": "integer",
"nullable": false,
"default_value": "nextval('posts_id_seq'::regclass)",
"is_primary_key": true,
"schema": "public",
"table": "posts"
},
{
"field": "title",
"name": "Title",
"type": "string",
"nullable": false,
"default_value": null,
"is_primary_key": false,
"schema": "public",
"table": "posts"
}
]
}Use this endpoint for lightweight introspection; use /api/fields/:collection for full daas-compatible field metadata (with meta and schema sub-objects).
Relations
Relations are discovered from foreign key constraints in PostgreSQL and from virtual M2M/O2M metadata stored in daas_relations.
All relations endpoints support ?limit=N to cap the number of returned items.
Get all relations
GET /api/relations
GET /api/relations?limit=5{
"data": [
{
"collection": "posts",
"field": "author",
"related_collection": "daas_users",
"meta": {
"many_collection": "posts",
"many_field": "author",
"one_collection": "daas_users",
"one_field": null,
"junction_field": null,
"sort_field": null
},
"schema": {
"table": "posts",
"column": "author",
"foreign_key_table": "daas_users",
"foreign_key_column": "id",
"constraint_name": "posts_author_foreign",
"on_update": "NO ACTION",
"on_delete": "SET NULL"
}
}
]
}Create a relation
POST /api/relations requires three fields: collection, field, and related_collection.
curl -X POST https://your-domain.com/api/relations \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"collection": "comments",
"field": "post_id",
"related_collection": "posts",
"schema": { "on_delete": "CASCADE" }
}'Returns 201 Created with the relation object.
Get relations for a collection
GET /api/relations/postsReturns all relations where the collection is the source.
Get a single relation
GET /api/relations/posts/authorReturns a single relation object.
Update a relation
PATCH /api/relations/posts/authorUpdates relation metadata and schema (on_delete, on_update). Returns 200.
Delete a relation
DELETE /api/relations/posts/authorReturns 204 No Content.
Interfaces
GET /api/interfaces returns available field interface definitions. Used by the Data Model UI to populate interface selection dropdowns.
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
grouped | boolean | false | Group interfaces by category |
type | string | — | Filter by field type (e.g. string, integer, boolean) |
localType | string | — | Filter by local type (e.g. m2o, m2m, file, presentation, group, workflow) |
supported | boolean | true | Set false to include unsupported interfaces |
Ungrouped response (default):
GET /api/interfaces{
"data": [
{ "id": "input", "name": "Input", "icon": "IconForms", "types": ["string","text","integer","bigInteger","float","decimal"], "group": "standard", "order": 1, "supported": true, "recommended": true },
{ "id": "input-multiline", "name": "Textarea", "icon": "IconAlignLeft", "types": ["string","text"], "group": "standard", "order": 2, "supported": true },
{ "id": "datetime", "name": "DateTime", "icon": "IconCalendar", "types": ["dateTime","date","time","timestamp"], "group": "standard", "order": 5, "supported": true, "recommended": true },
{ "id": "collection-item-dropdown", "name": "Collection Item", "icon": "IconDatabase", "types": ["string","text","integer","uuid"], "localTypes": ["m2o"], "group": "relational", "order": 1, "supported": true, "relational": true },
{ "id": "file-image", "name": "Image", "icon": "IconPhoto", "types": ["uuid"], "localTypes": ["file"], "group": "relational", "order": 7, "supported": true }
]
}Grouped response:
GET /api/interfaces?grouped=true{
"data": {
"interfaces": [ ... ],
"groups": [
{
"key": "standard",
"name": "Text & Numbers",
"interfaces": [
{ "id": "input", "name": "Input", "types": ["string","text","integer","bigInteger","float","decimal"], "group": "standard", "order": 1 },
{ "id": "datetime", "name": "DateTime", "types": ["dateTime","date","time","timestamp"], "group": "standard", "order": 5 }
]
},
{
"key": "selection",
"name": "Selection",
"interfaces": [ ... ]
},
{
"key": "relational",
"name": "Relational",
"interfaces": [ ... ]
},
{
"key": "presentation",
"name": "Presentation",
"interfaces": [ ... ]
},
{
"key": "group",
"name": "Groups",
"interfaces": [ ... ]
},
{
"key": "workflow",
"name": "Workflow",
"interfaces": [ ... ]
}
]
}
}Filtered:
GET /api/interfaces?type=string&localType=standardReturns only interfaces that support string fields and have a local type of standard.
Schema Migration
Snapshot, diff and apply let you promote data model changes between environments (development → staging → production). For a step-by-step guide, see Promoting Changes Between Environments.
Snapshot
Export the data model as a JSON snapshot:
GET /api/schema/snapshotQuery parameters:
| Parameter | Description |
|---|---|
includeCollections | Comma-separated collection names to include. Produces a scoped snapshot (version: 2). |
excludeCollections | Comma-separated collection names to leave out. Produces a scoped snapshot (version: 2). |
Response:
{
"data": {
"version": 1,
"directus": "11.12.0",
"vendor": "postgres",
"collections": [
{
"collection": "posts",
"meta": { "collection": "posts", "icon": "article", "note": "Blog posts" },
"schema": { "name": "posts", "schema": "public" }
}
],
"fields": [
{
"collection": "posts",
"field": "title",
"type": "string",
"meta": { "interface": "input", "required": true },
"schema": { "name": "title", "table": "posts", "data_type": "character varying", "max_length": 255, "is_nullable": false, "default_value": null }
}
],
"relations": [
{
"collection": "posts",
"field": "author",
"related_collection": "daas_users",
"meta": { "many_collection": "posts", "many_field": "author", "one_collection": "daas_users" },
"schema": { "table": "posts", "column": "author", "foreign_key_table": "daas_users", "foreign_key_column": "id", "on_delete": "SET NULL", "on_update": "NO ACTION" }
}
],
"timestamp": "2024-06-15T10:30:00.000Z",
"schema": "public",
"tables": [ { "table_name": "posts", "table_type": "BASE TABLE" } ],
"columns": [ { "table_name": "posts", "column_name": "id", "data_type": "integer" } ],
"foreign_keys": [ { "constraint_name": "posts_author_foreign", "table_name": "posts", "column_name": "author" } ]
}
}tables, columns, foreign_keys and the directus_collections / directus_fields / directus_relations aliases are kept for backwards compatibility.
Diff
Compare a snapshot (the desired state) against the live schema of the environment that receives the request:
POST /api/schema/diff?mode=mergeQuery parameters:
| Parameter | Description |
|---|---|
mode | merge (default): only creates and updates. mirror: also deletes collections, fields and relations that are not in the snapshot. Any other value returns 400. |
force | true skips the vendor check (the snapshot vendor must otherwise be postgres). |
Request body: the snapshot data object, either bare or wrapped as { "snapshot": ... } or { "data": ... }. collections, fields and relations must all be arrays.
Response:
{
"data": {
"hash": "8f3b20755919e17b8893d39572b9a7852c0049281729da842b1fe186e80b2713",
"collections": [
{ "collection": "comments", "diff": { "kind": "N", "rhs": { "collection": "comments", "meta": { ... }, "schema": { ... } } } }
],
"fields": [
{ "collection": "comments", "field": "body", "diff": { "kind": "N", "rhs": { ... } } },
{ "collection": "posts", "field": "title", "diff": { "kind": "E", "lhs": { ... }, "rhs": { ... } } }
],
"relations": []
}
}kind is N (new), E (edited: lhs is the current state, rhs the snapshot) or D (deleted, mirror mode only). hash identifies the current target schema and is required by apply.
Diff rules:
- System collections (
daas_*,directus_*,xtr_*) and database views are never included. - A scoped snapshot (
version: 2) never deletes collections. Inmirrormode it only deletes fields and relations inside the collections it contains. - Scope fields (foreign keys to
daas_scope_items) are never deleted. - Metadata that is missing from the snapshot is treated as “no change”.
A legacy snapshot that only has tables, columns and foreign_keys returns the legacy table/column/foreign-key diff instead:
{
"data": {
"tables": { "added": [ { "table_name": "comments", "table_type": "BASE TABLE" } ], "removed": [] },
"columns": {
"added": [ { "table_name": "comments", "column_name": "body", "data_type": "text", "is_nullable": "YES" } ],
"removed": [],
"changed": [
{
"key": "posts.title",
"current": { "data_type": "text", "is_nullable": "NO", "column_default": null },
"snapshot": { "data_type": "character varying", "is_nullable": "NO", "column_default": null }
}
]
},
"foreign_keys": { "added": [], "removed": [] }
}
}Apply
Apply a diff returned by POST /api/schema/diff. Send it unchanged, including hash:
POST /api/schema/applyRequest body:
{
"hash": "8f3b20755919e17b8893d39572b9a7852c0049281729da842b1fe186e80b2713",
"collections": [ ... ],
"fields": [ ... ],
"relations": [ ... ]
}The diff runs as a single transaction: tables and columns, foreign keys, row-level security for new tables (the same policies POST /api/collections applies) and the daas_collections / daas_fields / daas_relations metadata. Column types, lengths, precision, nullability and defaults are recreated exactly as introspected on the source.
| Status | Meaning |
|---|---|
| 204 | All changes applied |
| 400 | Invalid payload (for example, missing hash) or an unsupported change. Nothing was changed. |
| 409 | SCHEMA_MODIFIED: the target schema changed since the diff was generated. Regenerate the diff. |
| 500 | The database rejected a statement. The whole migration was rolled back. |
Unsupported changes (return 400):
- Adding, removing or changing a primary key, or changing auto-increment.
- Converting between an alias field and a database column.
- Enum and other user-defined column types, and array columns.
Unique constraint and index changes on existing columns are not migrated.
Legacy SQL operations
POST /api/schema/apply still accepts raw SQL operations, executed one at a time and stopping at the first failure (earlier operations stay applied):
{
"operations": [
{ "type": "sql", "sql": "CREATE TABLE comments (id SERIAL PRIMARY KEY, body TEXT, post_id INTEGER)" },
{ "type": "sql", "sql": "ALTER TABLE comments ADD CONSTRAINT comments_post_foreign FOREIGN KEY (post_id) REFERENCES posts (id) ON DELETE CASCADE" }
]
}Each operation must have "type": "sql" and a non-empty sql string. The response is 200 with { "data": { "applied": 2, "results": [...] } }.