Appearance
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
}
]
}| Field | Description |
|---|---|
id | UUID |
enabled | Whether the extension is loaded |
folder | Directory name under EXTENSIONS_PATH |
source | Where the extension comes from: local, registry, module |
bundle | Parent 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: callsextensionManager.reloadExtension(id)to hot-reload the extension - If
enabled = false: callsextensionManager.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 deregisterExtension Loading
When loading an extension:
- Reads the extension record from
odp_extensions - Resolves the extension folder path:
EXTENSIONS_PATH/{folder} - Dynamically imports the extension's main module
- Calls
extension.register(app, { services, schema, ... })or equivalent API - 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
| Column | Type | Description |
|---|---|---|
id | UUID PK | |
enabled | boolean | Whether the extension is active |
folder | string(255) | Directory name under EXTENSIONS_PATH |
source | string(255) | local, registry, or module |
bundle | UUID FK → odp_extensions | Parent bundle (self-referential) |
odp_extension_settings
| Column | Type | Description |
|---|---|---|
id | integer PK (auto-increment) | |
extension_key | string | Namespace key (e.g., system_auth_provider, system_storage_provider) |
name | string | Setting name within the namespace |
value | text/JSON | Configuration value |
Known extension keys:
system_auth_provider— SSO provider configurationssystem_storage_provider— Storage driver configurationssystem_mcp_settings— MCP server settings
Configuration
| Variable | Default | Description |
|---|---|---|
EXTENSIONS_PATH | ./extensions | Directory for extension folders |
EXTENSIONS_MUST_LOAD | "" | Comma-separated extension IDs that must load successfully (server fails to start if they don't) |
EXTENSIONS_AUTO_RELOAD | false | Watch for file changes and auto-reload extensions |
Writing an Extension
A minimal local extension:
extensions/
my-extension/
package.json
index.jspackage.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');