Skip to Content
ConnectSchema API

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

MethodPathStatusAdminDescription
GET/api/collections200NoList all collections
GET/api/collections/:name200NoGet collection metadata
GET/api/collections/:name/fields200NoList raw columns for a collection
POST/api/collections201YesCreate a table or virtual folder
PATCH/api/collections200YesBatch update collection metadata
PATCH/api/collections/:name200YesUpdate collection metadata
DELETE/api/collections/:name204YesDrop table
GET/api/fields200NoList all fields
GET/api/fields/:collection200NoList fields for a collection
GET/api/fields/:collection/:field200NoGet single field metadata
POST/api/fields/:collection201YesAdd a field
PATCH/api/fields/:collection/:field200YesUpdate a field
DELETE/api/fields/:collection/:field204YesRemove a field
GET/api/relations200NoList all relations
GET/api/relations/:collection200NoList relations for a collection
GET/api/relations/:collection/:field200NoGet single relation
POST/api/relations201YesCreate a relation
PATCH/api/relations/:collection/:field200YesUpdate a relation
DELETE/api/relations/:collection/:field204YesDelete a relation
GET/api/interfaces200NoList available field interfaces
GET/api/schema/snapshot200YesExport schema snapshot
POST/api/schema/diff200YesDiff a snapshot against the live schema
POST/api/schema/apply204YesApply a diff in one transaction

Common error codes:

StatusMeaning
201Created
204Deleted (no content)
400Bad request (missing fields, invalid payload, duplicate, cannot delete primary key)
401Unauthenticated
403Forbidden (admin-only endpoint)
404Not found
409Conflict (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" } ] }
FieldTypeDescription
collectionstringCollection/table name
schemaobjectSchema info — name (table name) and schema (PostgreSQL schema, typically "public")
metaobject | nullCollection metadata (icon, note, sort_field, etc.)
systembooleanWhether the collection is a system collection
typestringCollection type — "table" or "view"

Get a single collection

GET /api/collections/:name

Returns 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/:name

Updates metadata for a single collection. Response is 200 with the updated collection object.

Delete a collection

DELETE /api/collections/:name

Drops 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=10

Returns 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=5

Same response format as above, filtered to a single collection.

Get a single field

GET /api/fields/posts/title

Returns 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/summary

Updates field metadata and returns 200 with the updated field.

Delete a field

DELETE /api/fields/posts/summary

Returns 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/posts

Returns all relations where the collection is the source.

Get a single relation

GET /api/relations/posts/author

Returns a single relation object.

Update a relation

PATCH /api/relations/posts/author

Updates relation metadata and schema (on_delete, on_update). Returns 200.

Delete a relation

DELETE /api/relations/posts/author

Returns 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:

ParamTypeDefaultDescription
groupedbooleanfalseGroup interfaces by category
typestring—Filter by field type (e.g. string, integer, boolean)
localTypestring—Filter by local type (e.g. m2o, m2m, file, presentation, group, workflow)
supportedbooleantrueSet 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=standard

Returns 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/snapshot

Query parameters:

ParameterDescription
includeCollectionsComma-separated collection names to include. Produces a scoped snapshot (version: 2).
excludeCollectionsComma-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=merge

Query parameters:

ParameterDescription
modemerge (default): only creates and updates. mirror: also deletes collections, fields and relations that are not in the snapshot. Any other value returns 400.
forcetrue 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. In mirror mode 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/apply

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

StatusMeaning
204All changes applied
400Invalid payload (for example, missing hash) or an unsupported change. Nothing was changed.
409SCHEMA_MODIFIED: the target schema changed since the diff was generated. Regenerate the diff.
500The 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": [...] } }.