# Version 12

> Breaking changes may require action on your part before upgrading.

## 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`](/configuration/files) 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](https://tiptap.dev). 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`](/configuration/security-limits) 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`](/guides/connect/errors) 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`](/configuration/files) 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.

```js
schemaDiff(snapshot, true); // [!code --]
schemaDiff(snapshot, { force: true }); // [!code ++]
```

### `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](/self-hosting/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](#license-enforcement)
- [Draft Publishing Workflow](#draft-publishing-workflow)
- [Extension Compatibility](#extension-compatibility)

### 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](/licensing/overview) 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.

<callout icon="i-lucide-info">

Enforcement is immediate on new instances. Instances upgrading to Directus 12 get a 30-day [grace period](#post-upgrade-grace-period) from the time of upgrade, after which these are enforced unless a license that enables them is configured.

</callout>

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.

<callout icon="i-lucide-info">

**Existing customers** should contact [licensing@directus.com](mailto: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.

</callout>

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](https://directus.com/oig).

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.

<table>
<thead>
  <tr>
    <th>
      Removed
    </th>
    
    <th>
      Replacement
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        navigation.background
      </code>
      
      , <code>
        navigation.backgroundAccent
      </code>
      
      , <code>
        navigation.borderWidth
      </code>
      
      , <code>
        navigation.borderColor
      </code>
    </td>
    
    <td>
      <code>
        shell.background
      </code>
      
      , <code>
        shell.backgroundAccent
      </code>
      
      , <code>
        shell.borderWidth
      </code>
      
      , <code>
        shell.borderColor
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        header.background
      </code>
      
      , <code>
        header.borderWidth
      </code>
      
      , <code>
        header.borderColor
      </code>
    </td>
    
    <td>
      Same <code>
        shell.*
      </code>
      
       tokens as above
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        header.headline.foreground
      </code>
      
      , <code>
        header.headline.fontFamily
      </code>
    </td>
    
    <td>
      Removed without replacement
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        headerShadow
      </code>
      
      , <code>
        sidebarShadow
      </code>
      
       (<code>
        LayoutConfig
      </code>
      
      , <code>
        defineLayout()
      </code>
      
      )
    </td>
    
    <td>
      Removed without replacement
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        boxShadow
      </code>
      
       (header theme rules schema)
    </td>
    
    <td>
      Removed without replacement
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        section.toggle.borderWidth
      </code>
      
      , <code>
        section.toggle.borderColor
      </code>
    </td>
    
    <td>
      Section-level border tokens
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        navigation.project.borderColor
      </code>
      
      , <code>
        navigation.project.borderWidth
      </code>
      
      , <code>
        navigation.project.background
      </code>
    </td>
    
    <td>
      Removed without replacement
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        borderColorFocus
      </code>
      
      , <code>
        boxShadowHover
      </code>
      
      , <code>
        boxShadowFocus
      </code>
    </td>
    
    <td>
      Migrate to focus ring tokens (see below)
    </td>
  </tr>
</tbody>
</table>

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.
