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. Passes ApiExtensionContext with services, database, getSchema, emitter, env, logger, validateAppAccess
  5. 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

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

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 endpoint

This 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.ts

package.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 build

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

ODP Internal API Documentation