Extensions
Extensions add custom event hooks and REST endpoints to the platform. There are two types:
| Type | Location | Reload | Access |
|---|---|---|---|
| Runtime | Stored in daas_extensions table | Hot-reload, no deployment | All users |
| File-based | extensions/<name>/index.mjs | Requires server restart | Requires Docker image access |
Event Names
Both extension types subscribe to the same events. Use a specific collection prefix for targeted hooks, or the global form to catch all collections.
| Pattern | Example | Description |
|---|---|---|
items.create | — | Create in any collection |
items.update | — | Update in any collection |
items.delete | — | Delete in any collection |
items.read | — | Read in any collection |
items.query | — | Query (filter-only) |
{collection}.items.create | articles.items.create | Create in specific collection |
{collection}.items.update | articles.items.update | Update in specific collection |
versions.create | — | Content version created |
xtr.{category}.{action} | xtr.item.promote | Workflow action event |
Runtime Extensions
Runtime extensions are stored in the daas_extensions database table and hot-reloaded without deployment. They are ideal for dynamic business logic managed via MCP or the API.
Database schema (daas_extensions)
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
name | text | Human-readable name |
description | text | What the extension does |
event | text | Event name to subscribe to |
type | text | filter or action |
code | text | JavaScript code body |
status | text | active, inactive, or error |
sort | integer | Execution order |
timeout_ms | integer | Execution timeout in ms (default 5000) |
memory_limit_mb | integer | Memory limit in MB (default 64) |
last_error | text | Last error message, if any |
last_error_at | timestamptz | When the last error occurred |
execution_count | integer | Total number of executions |
last_executed_at | timestamptz | Last execution timestamp |
avg_execution_time_ms | numeric | Average execution time |
Code context
Filter hooks receive (payload, meta, context) and must return the payload:
// Auto-generate slug from title
if (!payload.slug && payload.title) {
payload.slug = payload.title.toLowerCase().replace(/\s+/g, '-');
}
return payload;Action hooks receive (meta, context) and the return value is ignored:
// Log creation
console.log(`Item created in ${meta.collection} by ${context.accountability?.user}`);Both have access to context.services:
// Items — CRUD on any collection
const svc = await context.services.items('articles');
const items = await svc.readByQuery({ filter: { status: { _eq: 'published' } } });
await svc.createOne({ title: 'New Article' });
await svc.updateOne(id, { status: 'draft' });
// Other services
const cols = await context.services.collections();
const flds = await context.services.fields();
const files = await context.services.files();
const vers = await context.services.versions();
const rels = await context.services.relations();
// Send email
await context.services.mail({ to: 'user@example.com', subject: 'Hi', html: '<p>Hello</p>' });
// Trigger / list cron jobs
await context.services.cron.trigger('job-id-or-name');
const jobs = await context.services.cron.list();
// Whitelisted environment variables
const apiKey = context.services.env.MY_API_KEY;
// Safe HTTP (domain-restricted — configure EXTENSION_ALLOWED_DOMAINS)
const res = await context.services.fetch('https://api.example.com/data');
// Raw Supabase client (bypasses RLS — use with caution)
const { data } = await context.services.supabase.from('table').select('*');Managing via MCP
The extensions MCP tool lets AI agents manage runtime extensions:
// Create
{ "action": "create", "name": "Auto-slug", "event": "articles.items.create",
"type": "filter", "code": "...", "status": "inactive" }
// Test before activating
{ "action": "test", "code": "return { ...payload, tested: true };",
"type": "filter", "event": "articles.items.create",
"testPayload": { "title": "Hello World" } }
// Activate
{ "action": "activate", "id": "extension-uuid" }
// List
{ "action": "list", "status": "active" }Always test an extension before activating it. A broken filter hook can block write operations for the affected collection.
File-Based Extensions
File-based extensions require access to the BuildPad Docker image to mount the extensions/ directory. Contact the BuildPad team to get access.
File-based extensions are JavaScript modules placed in the extensions/ directory. They are loaded at server startup and require a restart to pick up changes.
Structure
extensions/
└── my-extension/
└── index.mjsModule format
Every extension exports a register(sdk) function:
// extensions/my-extension/index.mjs
export function register(sdk) {
const { emitter, createItemsService, registerRoutes } = sdk;
// Action hook — runs after item creation
emitter.onAction('articles.items.create', async (meta, context) => {
console.log('Article created:', meta.key);
});
// Filter hook — runs before item creation, can modify payload
emitter.onFilter('articles.items.create', async (payload, meta, context) => {
if (!payload.title) throw new Error('Title is required');
return {
...payload,
slug: payload.title.toLowerCase().replace(/\s+/g, '-'),
};
});
}Extensions are loaded at startup. Restart the server to pick up new file-based extensions.
SDK reference
| SDK export | Description |
|---|---|
emitter.onAction(event, handler) | Register non-blocking action hook |
emitter.onFilter(event, handler) | Register blocking filter hook |
emitter.offAction(event, handler) | Unregister action hook |
emitter.offFilter(event, handler) | Unregister filter hook |
createItemsService(collection) | Create an ItemsService instance |
createVersionService() | Create a VersionService instance |
registerRoutes(config) | Register custom REST endpoints |
Custom REST endpoints
export function register(sdk) {
sdk.registerRoutes({
name: 'my-extension',
basePath: '/customers', // accessible at /api/ext/customers/*
description: 'Customer API',
routes: [
{
method: 'GET',
path: '/',
description: 'List customers',
requiredPermission: 'customers.read',
handler: async (request, context) => {
const { supabase, accountability } = context;
const { data } = await supabase.from('customers').select('*');
return Response.json({ data });
},
},
{
method: 'POST',
path: '/',
requiredPermission: 'customers.create',
handler: async (request, context) => {
const body = await request.json();
// ... create logic
return Response.json({ data: customer }, { status: 201 });
},
},
],
});
}All extension routes are available at /api/ext/{basePath}/.
TypeScript types
import type {
FilterHandler,
ActionHandler,
EventContext,
Accountability,
} from '@microbuild/sdk';