Version 12

Breaking changes may require action on your part before upgrading.

Version 12.4.0

Inactive collections reject reads and writes

A collection whose status is Inactive can no longer be read or written through the API. Previously the status only affected how the collection appeared in the Data Studio. Requests against an inactive collection now fail with a COLLECTION_INACTIVE error (403) for users who have permission on the collection, and with a generic FORBIDDEN error for everyone else.

The enforcement applies to REST, GraphQL, WebSockets, flow operations, and relational writes nested inside another collection's payload. Queries that read, filter, or sort through a relation into an inactive collection are rejected as well. Wildcard field selections (* and *.*) drop one-to-many and many-to-any aliases that point at inactive collections, while a many-to-one field keeps returning its foreign key value.

Schema management is unaffected, so you can still change an inactive collection's fields, relations, and settings. In the Data Studio, inactive collections remain visible but cannot be selected or used.

Collections are deactivated through the licensing resolution flow when an instance is over its collection limit. To use one again, set its status back to Active in the collection settings. See Inactive Collections for details.

Update and delete by query enforce read permissions

updateByQuery and deleteByQuery previously resolved the affected item keys without applying the caller's read permissions. They now resolve the affected items with read permissions enforced, which changes what a request can touch:

  • Read access to the collection's primary key field is required. Roles with update or delete permissions but no read permission on the primary key now receive a FORBIDDEN error.
  • The affected items are limited to those the role can read, regardless of its update or delete rights. Items that a role can update but cannot read are skipped.
  • Fields used in the query's filter and sort must be readable by the role, because the query now runs as an ordinary read.
  • Saving nested one-to-many items requires read access on the child collection.

This applies to PATCH and DELETE requests that pass a query, to the equivalent GraphQL mutations, and to the Update Items and Delete Items flow operations when they run with a non-admin accountability. Review policies that grant update or delete access without a matching read permission.

Map layout and map interface require WebGL2

The Data Studio's map layout and map interface now run on MapLibre GL JS 6, which requires WebGL2. Browsers that only support WebGL1, chiefly Safari 14 and earlier and older Android devices, no longer render maps. All other Studio functionality is unaffected in those browsers.

Dragging the map with a pointer or mouse wheel still pins the hovered item popup to the cursor. Touch drags no longer reposition the popup.

@directus/themes requires @unhead/vue 3

The @directus/themes package now lists @unhead/vue 3.x as a peer dependency. If you pair the package with your own Unhead instance, upgrade it and follow the Unhead v2 and v3 migration guides. Note that createHead is now imported from @unhead/vue/client and template params require registering TemplateParamsPlugin.

System type definitions corrected

Several type definitions in @directus/types and @directus/sdk were out of date with the fields they describe. TypeScript projects that read these fields without a null check may fail to compile after upgrading.

In @directus/types, the following are now nullable:

  • Comment.user_created and Comment.user_updated
  • FlowRaw.user_created
  • OperationRaw.options and OperationRaw.user_created
  • Preset.collection
  • Role.description
  • ContentVersion.hash
  • Share.name, Share.role, Share.password, Share.user_created, Share.date_created, and Share.times_used

Policy.enforce_tfa in @directus/types is no longer nullable.

In @directus/sdk:

  • DirectusRole.parent, DirectusVersion.hash, DirectusRelation.meta, and DirectusRelation.schema are now nullable.
  • DirectusPolicy.ip_access and DirectusRelation.meta.one_allowed_collections are now typed as string[] instead of string.
  • The unused helper types ToTuple and TupleToUnion have been removed.

The SDK also validates that readRelationByCollection, createField, deleteCollection, utilsExport, utilsImport, utilitySort, triggerFlow, and readShareInfo receive non-empty parameters, and throws before sending a request when they do not.

Version 12.3.0

Update and Delete operations no longer target all items by default

The Update Items and Delete Items flow operations previously affected every item in a collection when their targeting parameters were empty or missing. They now return null instead of running against the whole collection.

If a flow relied on the previous behavior to update or delete all items, set an explicit query: {"limit": -1}.

These operations now also throw an error when both key and query are defined at the same time, and Update Items throws when either parameter is combined with a batch payload. Pass only one targeting method per operation.

Storage exists() throws on connection failures

The storage driver's exists() method previously returned false for any failure, including connection timeouts and authentication errors. It now throws on these failures so a transient problem is no longer mistaken for a missing object. Extensions and callers that use exists() must handle the thrown error.

If you use S3, grant the s3:ListBucket permission so the driver can tell a missing object apart from a permission error.

Image transformation output default raised to 6000px

ASSETS_TRANSFORM_IMAGE_MAX_OUTPUT_DIMENSION now defaults to 6000, up from 3000 in 12.2.0. Transformations that were rejected between these limits are now allowed by default. To keep the previous 3000 limit, set ASSETS_TRANSFORM_IMAGE_MAX_OUTPUT_DIMENSION to 3000 explicitly.

Docker images use the bundled pm2

The Docker image now runs the pm2 version bundled with it. If your custom CMD calls pm2-runtime directly, route it through docker-entrypoint.cjs instead. The startup sequence is unchanged: bootstrap, then pm2-runtime.

Version 12.2.0

WYSIWYG editor rebuilt on Tiptap

The WYSIWYG interface (input-rich-text-html) has replaced its TinyMCE editor with Tiptap. The interface id, option keys, and HTML storage format are unchanged, so existing fields continue to work without migration. Two changes may require action.

Content is normalized on first edit

The editor's schema now defines which HTML it can represent. When you open an existing value, change it, and save, markup the schema does not support is normalized or removed. This only happens when a field is edited and saved. Values you do not touch are left as they are.

The editor supports the following HTML:

  • Text blocks and marks: <p>, <h1>-<h6>, <ul>, <ol>, <li>, <blockquote>, <pre>, <code>, <a>, <strong>/<b>, <em>/<i>, <u>, <s>/<del>, <sub>, <sup>, <span>, <br>, and <hr>.
  • Media and tables: <img>, <video>, <audio>, <iframe>, and <table> with its row and cell tags.
  • Semantic tags: <section>, <article>, <figure>/<figcaption>, <details>/<summary>, <dl>/<dt>/<dd>, <mark>, and <abbr>.
  • class, id, title, role, lang, dir, data-*, and aria-* attributes on supported elements.

The following are changed or dropped on save:

  • HTML comments.
  • Inline style values outside the editor's supported set.
  • <script> and <style> tags.
  • Tags not listed above, including <div> wrappers. Their content is kept where possible, but the tags themselves are removed.

When a field's stored HTML contains markup that would be normalized, the editor loads read-only and shows a notice. Selecting the field opens a dialog with a diff of what would change, and three choices:

  • Keep Read-only leaves the value untouched.
  • Edit Anyway unlocks the editor. Unsupported markup is removed when you save.
  • Edit Raw HTML switches the field to a raw HTML editor, so you can edit the value without any normalization.

If you style or process stored HTML by tag, class, or attribute, review affected fields before saving.

Options Override (tinymceOverrides) is deprecated

This option passed raw TinyMCE configuration to the editor and no longer has any effect. Existing values are ignored and log a deprecation warning in the browser console, and the option is hidden for new fields. It will be removed in a future release. Configure the toolbar, custom formats, font families, and font sizes through the interface's dedicated options instead.

App Access policies read a limited set of settings fields

The minimal permissions attached to a policy with App Access enabled previously granted read access to every field on directus_settings, including admin-only configuration and AI provider credentials. New policies now grant read access to only the fields a non-admin user needs:

id, project_url, project_logo, module_bar, storage_asset_transform, storage_asset_presets, custom_aspect_ratios, basemaps, mapbox_key, visual_editor_urls, collaborative_editing_enabled, report_error_url, ai_openai_compatible_models, ai_openai_allowed_models, ai_anthropic_allowed_models, ai_google_allowed_models, mcp_prompts_collection, default_save_action, ai_translation_default_model, ai_translation_glossary, and ai_translation_style_guide.

Existing policies are not modified, so upgrading does not change what your current users can read. This means any policy created before this release still exposes the full settings collection, including ai_openai_api_key and the other provider keys. Audit your App Access policies and scope the directus_settings read permission to the fields you need.

If an extension or frontend of yours reads a settings field that a new policy no longer grants, add that field explicitly to the policy's read permission.

Imports are capped at 50mb by default

IMPORT_MAX_FILE_SIZE limits the size of files uploaded to the import endpoints, and of schema snapshots uploaded to /schema/diff and /schema/apply. It defaults to 50mb.

Imports were previously unrestricted, so a file that used to be processed now fails with 413 Content Too Large once it passes the limit. Raise IMPORT_MAX_FILE_SIZE to restore the previous behavior.

Image transformation output is capped at 3000px

ASSETS_TRANSFORM_IMAGE_MAX_OUTPUT_DIMENSION limits the pixel dimensions a transformation is allowed to output, and defaults to 3000. A transformation projecting a width or height above the limit is rejected with an ILLEGAL_ASSET_TRANSFORMATION error.

The limit applies at every step of a transformation rather than only to the final dimensions, so a preset that scales an image up to 10000px before scaling it back down is rejected. Review your storage asset presets and the transformation parameters your frontend requests, then raise ASSETS_TRANSFORM_IMAGE_MAX_OUTPUT_DIMENSION if you need larger output.

Valueless boolean query flags now count as true

Boolean query flags are now read through a shared helper that treats a flag carrying no value (?flag) as true. Two endpoints change behavior as a result:

  • POST /utils/import/:collection?background previously evaluated to false, so the import ran synchronously. It now runs in the background.
  • POST /schema/apply?force previously evaluated to false, so version and database vendor checks still applied. It now bypasses them.

Pass an explicit value (?background=false, ?force=false) wherever you relied on the previous behavior.

POST /schema/diff changes in the opposite direction. It previously treated the mere presence of force as true, so even ?force=false bypassed the checks. It now respects the value you pass.

SDK schemaDiff takes an options object

schemaDiff accepted force as a positional second argument. It now takes an options object, so the new mode parameter can be passed alongside it.

schemaDiff(snapshot, true); schemaDiff(snapshot, { force: true }); 

DeploymentProjectsService.readByExternalId takes a deployment ID

External IDs are only unique within a provider, so this method now takes the deployment ID as its first argument: readByExternalId(deploymentId, externalId). Update any extension that calls it directly.

Version 12.1.0

Removal of /hash endpoints

The /utils/hash/generate and /utils/hash/verify endpoints have been removed. Equivalent behavior can be achieved by using extensions endpoints along with the argon2 library.

Sensitive GraphQL system mutations are now limited to single use

Sensitive GraphQL system mutations are now limited to one per request, set by the GRAPHQL_SINGLE_USE_MUTATIONS environment variable.

GraphQL WebSocket restrictions now match the HTTP endpoint

GraphQL over WebSockets now applies the same restrictions as the HTTP endpoint. When introspection is disabled, validation hints are hidden.

CORS_ORIGIN is now enforced for WebSocket connections

WebSocket connections are now validated against CORS_ORIGIN. If you use WebSockets, make sure CORS_ORIGIN includes your clients' origins, or those connections will now be rejected.

Hardened upload path validation

Upload path validation has been hardened to prevent writes to extension and temporary storage directories. Uploads that previously targeted these protected directories will now be rejected.

Hardened Docker image and new distroless variant

The published Docker image has been hardened: it now applies outstanding OS-level patches at build time and drops npm and npx from the runtime. If your setup relies on npm or npx inside the container (for example, to install extensions at runtime), it will no longer work.

A new distroless Docker Hardened Image (DHI) variant is also published alongside the standard image, under a -dhi tag suffix.

See Hardened Images for details on both images, as well as instructions for installing extensions and running the CLI inside them.

Version 12.0.0

Jump to:

License Enforcement

Directus 12 introduces active license enforcement. Self-hosted instances run on the Core tier by default. Higher limits and additional features require a valid license. See Licensing for a complete overview.

This change affects instances previously using features that now require a license, including:

  • SSO — SSO login will no longer work. Users who authenticate through SSO will be unable to log in and must be converted to email and password users to regain access.
  • Custom permission rules — custom rules on access policies will be ignored.
  • Custom or self-hosted LLMs — connections to custom LLMs will no longer work.
Enforcement is immediate on new instances. Instances upgrading to Directus 12 get a 30-day grace period from the time of upgrade, after which these are enforced unless a license that enables them is configured.

If your instance uses any of these features, add a license that includes them to continue to do so. If your instance uses only Core tier features, no action is required.

Post-upgrade Grace Period

The grace period exists to ensure uninterrupted service for existing customers who upgrade before obtaining a license. If your instance is above core tier limits when you upgrade to Directus 12, you enter a 30-day grace period during which everything continues to operate normally. Admins see a reminder on every login.

Existing customers should contact licensing@directus.com to obtain your license. This is the right action for any existing customer who has not yet received a license, or upgrades and sees the grace period reminder.

If you are not an existing customer and do not intend to add a license, reduce usage to within core limits before the grace period ends. Note that free commercial use may be available through the Open Innovation Grant.

If the grace period ends without resolution, the resolution flow is triggered on next admin login and certain APIs are locked down. To resolve, either add a license that covers your usage and features, or bring usage and feature use within Core tier limits.

Locked-Down Behavior

When an instance is over its entitlement limits and the grace period has elapsed:

  • Prevents common API operations (e.g. /items endpoints)
  • The GraphQL, WebSockets, and MCP APIs are disabled.
  • /login is blocked for non-admin users.
  • SSO continues to work for admins so the license can be resolved.

No data is deleted as a result of enforcement — access is restricted via deactivation or blocking only.

IP_TRUST_PROXY Default Changed to false

The IP_TRUST_PROXY default has been changed from true to false. If you run Directus behind a reverse proxy and rely on X-Forwarded-For (or similar) headers for client IP resolution, you must now explicitly set IP_TRUST_PROXY to true or a more specific trust configuration.

Draft Publishing Workflow

Published items in versioned collections are now locked from direct editing

In collections with content versioning enabled, published items can no longer be edited directly. To make changes, switch to the draft version using the new header action button. The Studio also handles this transition automatically:

  • Creating a new item in a versioned collection opens a virtual draft. The URL takes the form collection/+?version=draft.
  • Editing a published item in a versioned collection automatically switches the editor context to draft mode.

?version=main renamed to ?version=published

The query parameter used to retrieve the published version of an item is now ?version=published. The previous ?version=main value continues to work, so existing integrations are not broken — but new code should use ?version=published.

Collection status replaced with an archived boolean

The collection-level status string has been replaced with an archived boolean for newly created collections. Existing collections that use the string-based status field continue to work as before, so no migration is required.

Publishing a version no longer requires a second confirmation

Publishing a version previously required two confirmations — one in the comparison modal, and a second confirmation dialog after it. The second dialog has been removed: confirming the changes in the comparison modal now publishes the item directly.

Extension Compatibility

The Studio has had a design refresh in Directus 12. Most extensions are unaffected, but theme and interface extensions may need updates. The following notices are grouped by area.

Removed and replaced theme properties

The following theme properties have been removed. Theme extensions that reference them will continue to load but the affected styling will no longer have any effect.

RemovedReplacement
navigation.background, navigation.backgroundAccent, navigation.borderWidth, navigation.borderColorshell.background, shell.backgroundAccent, shell.borderWidth, shell.borderColor
header.background, header.borderWidth, header.borderColorSame shell.* tokens as above
header.headline.foreground, header.headline.fontFamilyRemoved without replacement
headerShadow, sidebarShadow (LayoutConfig, defineLayout())Removed without replacement
boxShadow (header theme rules schema)Removed without replacement
section.toggle.borderWidth, section.toggle.borderColorSection-level border tokens
navigation.project.borderColor, navigation.project.borderWidth, navigation.project.backgroundRemoved without replacement
borderColorFocus, boxShadowHover, boxShadowFocusMigrate to focus ring tokens (see below)

CSS variable migrations:

  • --theme--navigation--* and --theme--header--*--theme--shell--*
  • --theme--form--field--input--border-color-focus and --theme--form--field--input--box-shadow-focus--theme--form--field--input--focus-ring-color

The focus ring is now implemented via the CSS outline property rather than border/box-shadow.

Component deprecations

The following components and props have been deprecated. Existing usage continues to function — extensions will see Volar hints — but should be migrated.

  • <v-resizeable> — migrate to @directus/vue-split-panel or an alternative.
  • <v-breadcrumb> — deprecated globally.
  • <v-drawer>: the subtitle prop, subtitle slot, header:append slot, and actions:append slot. Use the new actions:primary slot for primary call-to-action components.
  • v-button rounded prop — removed. Existing usage continues to function but buttons render as rounded rectangles instead of circles.

Slot deprecations on the header bar

The private view header bar has changed:

  • The #headline slot has been deprecated. Existing usage still renders in the #title:prepend zone.
  • The actions:append slot has been deprecated. Existing usage still renders in the secondary-actions zone, but should be migrated to the new actions:primary slot.

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