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
- Passes
ApiExtensionContextwithservices,database,getSchema,emitter,env,logger,validateAppAccess - Extension registers its hooks, routes, and listeners using the provided context
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
Use the CLI to scaffold a new extension:
bash
# Create a bundle (hooks + endpoints)
odp extension:create -n my-extension -t bundle
# Or a single hook / endpoint
odp extension:create -n my-hook -t hook
odp extension:create -n my-endpoint -t endpointThis generates the project structure, package.json with odp-extension metadata, TypeScript config, and source templates.
A minimal hook extension:
extensions/
my-hook/
package.json
tsconfig.json
src/
index.tspackage.json:
json
{
"name": "my-hook",
"version": "1.0.0",
"type": "module",
"odp-extension": {
"id": "my-hook",
"type": "hook"
},
"scripts": {
"build": "tsup src/index.ts --format esm --out-dir dist --no-splitting",
"dev": "tsup src/index.ts --format esm --out-dir dist --no-splitting --watch src"
},
"devDependencies": {
"tsup": "^8.0.0",
"typescript": "^5.7.0",
"@odp/extensions-sdk": "*"
}
}src/index.ts:
typescript
import { defineHook } from '@odp/extensions-sdk';
export default defineHook((context, { logger }) => {
context.action('items.create', async (meta) => {
(logger as any).info({ collection: meta.collection, key: meta.key }, 'Item created');
});
context.filter('items.create', async (payload, meta) => {
if (meta.collection === 'articles') {
payload.status = payload.status ?? 'draft';
}
return payload;
});
});Build and register:
bash
cd extensions/my-hook
npm install
npm run buildThe Extension Manager auto-discovers extensions with valid odp-extension metadata. To manually register:
sql
INSERT INTO odp_extensions (id, enabled, folder, source)
VALUES (gen_random_uuid(), true, 'my-hook', 'local');For the full development guide including event types, endpoint routing, runtime context, and examples, see Extension Development Guide.