Version 12

Breaking changes may require action on your part before upgrading.

Version 12.5.0

Stricter IP access validation

Policy IP Access (ip_access) values and the IMPORT_IP_DENY_LIST environment variable are now validated more strictly. Each entry must be one of the following:

  • An IPv4 or IPv6 address, such as 192.168.1.10.
  • A range of two full addresses in the same family, such as 10.0.0.1-10.0.0.50, with no spaces around the -.
  • A CIDR block with a numeric prefix, such as 10.0.0.0/24.

Subnets with a malformed prefix, such as 10.0.0.0/ 24 or 10.0.0.0/+24, were previously accepted and are now rejected. Saving a policy with an invalid entry fails with a validation error.

A policy that already stores an invalid entry is not migrated. Every request from a user with that policy fails with a 500 error, including logins. Before upgrading, review the ip_access column of directus_policies and correct any invalid entries. If an admin's own policy is affected, correct the value directly in the database or from another admin account that does not have that policy.

If any IMPORT_IP_DENY_LIST entry is invalid, Directus logs a warning and denies every outbound request that checks the list. This includes file imports, the Request URL flow operation, and requests made by sandboxed extensions.

Two-factor enforcement follows effective policies

When a user without 2FA logs in, Directus decides whether to require 2FA setup from the policies that apply to that request. Previously it only checked policies attached directly to the user's primary role, and ignored IP access. This changes who is asked to set up 2FA:

  • A Require 2FA policy attached to a parent role, or assigned directly to the user, now requires 2FA.
  • A Require 2FA policy with an IP allowlist only requires 2FA when the user logs in from an allowed IP address.

This mainly affects SSO logins and API clients that read the enforce_tfa claim in the access token. The Data Studio already used effective policies. See Two-Factor Authentication.

CACHE_SCHEMA_MAX_ITERATIONS removed

CACHE_SCHEMA_MAX_ITERATIONS has been removed. CACHE_SCHEMA_SYNC_TIMEOUT now limits the whole schema build, both for the process building the schema and for processes waiting on it. Its default has increased from 10000 to 60000 milliseconds.

When a schema build exceeds the timeout, requests will fail and Directus logs a warning. If you set a lower value, make sure it is long enough for your schema to finish building.

License endpoint returns invalid_reason

The GET /license response no longer includes downgrade_reason. Read invalid_reason instead. It reports why a license was downgraded, and also why a license that is still in effect could not be renewed.

invalid_reason previously reported only expired, canceled, or suspended. It can now also be verification, invalid_key, activation_limit, binding_mismatch, or unavailable. A new token_expires_at field gives the Unix timestamp when the current license lapses. See Why a License Could Not Be Renewed.

License error classes removed from @directus/errors

@directus/errors no longer exports LicenseManagedByEnvError, LicenseOfflineUnsupportedError, LicenseResolveIncompleteError, LicenseServiceUnavailableError, or LicenseImmutableError. The LICENSE_MANAGED_BY_ENV, LICENSE_OFFLINE_UNSUPPORTED, LICENSE_RESOLVE_INCOMPLETE, and LICENSE_SERVICE_UNAVAILABLE error codes are removed too. The API never returned these codes, so only extensions that import them are affected.

License request failures now return one of the following:

  • LICENSE_INVALID (400), with a failure extension of expired, canceled, suspended, invalid_key, activation_limit, or binding_mismatch.
  • INVALID_PAYLOAD, FORBIDDEN, REQUESTS_EXCEEDED, or SERVICE_UNAVAILABLE.

BINDING_MISMATCH and activation limit failures previously returned FORBIDDEN, and now return LICENSE_INVALID.

File storage must be a configured location

Creating, uploading, or updating a file with a storage value that is not listed in STORAGE_LOCATIONS now fails with an INVALID_PAYLOAD error. The match is exact and case-sensitive. Scripts or migrations that write file records pointing at an unconfigured location must use a configured one.

Resumable (TUS) uploads that replace an existing file only work for files stored in the first location in STORAGE_LOCATIONS.

System type definitions corrected

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

In @directus/sdk:

  • DirectusNotification.id is now typed as number.
  • DirectusField.schema is now nullable, and its schema, comment, and foreign_key_schema properties are now optional.
  • FieldMetaConditionOptionType has been removed. Condition options is now Record<string, any>.
  • FieldMetaConditionType.hidden, readonly, required, and options are now optional, and rule is typed as a filter.
  • All ExtensionSchema properties are now optional, and DirectusExtension.schema can also be an ExtensionSchemaEntry.

In @directus/types:

  • Notification.id is now typed as number.
  • Notification.status and Notification.timestamp are now nullable.

The SDK's readActivity, readNotification, readRevision, readPermission, updatePermission, deletePermission, and readItemPermissions functions now throw before sending a request when the key or collection is null or undefined. Previously they sent a request to a path such as /permissions/null.

Version 12.4.0

Upgrade to 12.4.1 instead of 12.4.0 In 12.4.0, reading directus_folders as a non-admin user fails with a 500 error, which breaks the File Library for non-admin users and any GET /folders request made with a non-admin token. 12.4.1 fixes this and has no breaking changes of its own. The breaking changes below apply to both versions.

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.

Flow folders are stored in directus_folders

Flows can now be organized into folders, which are stored in directus_folders alongside File Library folders. A new type field tells them apart. It is files for File Library folders and flows for Flow folders. Existing folders are set to files.

Flow folders are admin-only. Non-admin requests to GET /folders never return them. Admin requests, including those made with an admin's static token, return both kinds. If you build a folder tree from /folders with an admin token, filter on the folder type:

GET /folders?filter[type][_eq]=files

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.
  • Linked images (an <a> with href, target, and rel wrapping an <img>), as of 12.5.0. Earlier 12.x releases treat this markup as unsupported.

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.

TinyMCE is no longer bundled with the Data Studio. Custom TinyMCE plugins, skins, and content CSS no longer apply, and the global tinymce object is no longer available to extensions.

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.

/server/health Requires Authentication

Unauthenticated requests to /server/health now fail with a 403 FORBIDDEN error. Previously, the endpoint returned the overall health status to anyone. Load balancer probes, uptime monitors, and container health checks that call /server/health without a token will fail after upgrading, which can mark the instance as unhealthy or trigger an automatic rollback.

Update each health check based on what it needs to verify:

  • Liveness - switch to /server/ping. It stays public and returns pong once the HTTP server is running. It does not check the database, Redis, storage, or email.
  • Dependency status - keep /server/health and send an access token with the request, for example a static token for a dedicated user. Non-admin users receive only the overall status. Admin users receive the full report, including individual checks.

The health check service has also changed in the following ways:

  • Results are cached for HEALTHCHECK_CACHE_TTL (default 5m) and shared across instances in multi-instance deployments. A reported status can lag behind the real state of a dependency by up to this duration.
  • The cache, rateLimiter, and rateLimiterGlobal checks have been replaced by a single redis check, reported under the redis: prefix. Update any monitoring that reads individual check names.
  • HEALTHCHECK_ENABLED and HEALTHCHECK_SERVICES let you disable the endpoint or limit which services it checks.

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