Skip to content

Extensions System

Overview

The ODP extensions system allows custom functionality to be loaded into the API at runtime. Extensions are stored in the filesystem under EXTENSIONS_PATH and registered in the odp_extensions database table.

Extensions can be enabled/disabled individually through the admin API. When enabled, the ExtensionManager loads the extension's code dynamically. When disabled, the extension's hooks are unregistered without requiring a server restart.


Extension Types

Extensions can provide:

  • API hooks — Register custom route handlers, middleware, and emitter listeners
  • App modules — Register new app permission modules
  • Custom endpoints — Add routes to the Fastify instance
  • Settings providers — Provide configuration settings to other parts of the system

Endpoints

GET /extensions

List all registered extensions.

Auth required: Yes (no specific permission required for listing)

Response:

json
{
  "data": [
    {
      "id": "extension-uuid",
      "enabled": true,
      "folder": "my-custom-extension",
      "source": "local",
      "bundle": null
    },
    {
      "id": "another-uuid",
      "enabled": false,
      "folder": "analytics-plugin",
      "source": "module",
      "bundle": null
    }
  ]
}
FieldDescription
idUUID
enabledWhether the extension is loaded
folderDirectory name under EXTENSIONS_PATH
sourceWhere the extension comes from: local, registry, module
bundleParent bundle UUID (if part of a bundled extension)

GET /extensions/:id

Get a single extension by ID.

Auth required: Yes

Response: Single extension object.

Error: 404 Not Found if extension does not exist.


PATCH /extensions/:id

Enable or disable an extension.

Auth required: Yes (admin typically required)

Request Body:

json
{
  "enabled": true
}

Response: Updated extension object.

Business Logic:

  • If enabled = true: calls extensionManager.reloadExtension(id) to hot-reload the extension
  • If enabled = false: calls extensionManager.unloadExtension(id) to remove hooks and routes

This enables hot reload — extensions can be toggled without restarting the server.


Extension Settings

Extensions can store their configuration in odp_extension_settings. This is used by the auth providers and storage providers as well as custom extensions.

GET /extension-settings

List all extension settings (admin required).

Query Parameters:

  • extension_key — Filter by extension key

Response:

json
{
  "data": [
    {
      "id": 1,
      "extension_key": "system_auth_provider",
      "name": "google",
      "value": { "driver": "openid", "clientId": "...", "clientSecret": "..." }
    }
  ]
}

Extension Manager

The ExtensionManager singleton manages the extension lifecycle:

extensionManager.initialize()   → scan EXTENSIONS_PATH, load all enabled extensions
extensionManager.reloadExtension(id) → unload + re-load a single extension
extensionManager.unloadExtension(id) → remove hooks and deregister

Extension Loading

When loading an extension:

  1. Reads the extension record from odp_extensions
  2. Resolves the extension folder path: EXTENSIONS_PATH/{folder}
  3. Dynamically imports the extension's main module
  4. Calls extension.register(app, { services, schema, ... }) or equivalent API
  5. Extension registers its hooks, routes, and listeners

Auto-Reload

If EXTENSIONS_AUTO_RELOAD = true, the manager watches the extensions directory and reloads changed extensions automatically.


Data Model

odp_extensions

ColumnTypeDescription
idUUID PK
enabledbooleanWhether the extension is active
folderstring(255)Directory name under EXTENSIONS_PATH
sourcestring(255)local, registry, or module
bundleUUID FK → odp_extensionsParent bundle (self-referential)

odp_extension_settings

ColumnTypeDescription
idinteger PK (auto-increment)
extension_keystringNamespace key (e.g., system_auth_provider, system_storage_provider)
namestringSetting name within the namespace
valuetext/JSONConfiguration value

Known extension keys:

  • system_auth_provider — SSO provider configurations
  • system_storage_provider — Storage driver configurations
  • system_mcp_settings — MCP server settings

Configuration

VariableDefaultDescription
EXTENSIONS_PATH./extensionsDirectory for extension folders
EXTENSIONS_MUST_LOAD""Comma-separated extension IDs that must load successfully (server fails to start if they don't)
EXTENSIONS_AUTO_RELOADfalseWatch for file changes and auto-reload extensions

Writing an Extension

A minimal local extension:

extensions/
  my-extension/
    package.json
    index.js

package.json:

json
{
  "name": "my-extension",
  "version": "1.0.0",
  "main": "index.js",
  "directus:extension": {
    "type": "hook",
    "path": "index.js"
  }
}

index.js:

javascript
export default function registerHook({ filter, action }) {
  // Listen for item creation
  action('items.create', ({ collection, key, payload }) => {
    console.log(`Created ${key} in ${collection}`);
  });

  // Filter item before creation
  filter('items.create', (payload, { collection }) => {
    if (collection === 'articles') {
      payload.status = payload.status ?? 'draft';
    }
    return payload;
  });
}

To register the extension, insert a record into odp_extensions:

sql
INSERT INTO odp_extensions (id, enabled, folder, source)
VALUES (gen_random_uuid(), true, 'my-extension', 'local');

ODP Internal API Documentation