Event Hooks
The event hook system lets you subscribe to item lifecycle operations across all collections. There are two hook types:
| Type | Blocking | Use case |
|---|---|---|
| Filter hook | ✅ Yes | Modify payload before an operation — validation, transformation, enrichment |
| Action hook | ❌ No | Side effects after an operation — logging, notifications, cache invalidation |
Event Names
Events follow the pattern [collection].items.[operation] or the global items.[operation].
| Event | Description |
|---|---|
items.query | Before query execution — can modify query params |
items.read | After query, before return — can modify results |
items.create | Item creation (all collections) |
items.update | Item update (all collections) |
items.delete | Item deletion (all collections) |
{collection}.items.query | Query for a specific collection |
{collection}.items.read | Read for a specific collection |
{collection}.items.create | Create for a specific collection |
{collection}.items.update | Update for a specific collection |
{collection}.items.delete | Delete for a specific collection |
versions.create | Content version created |
Event Coverage
Not all built-in API routes emit events. Routes that use ItemsService internally emit events; routes using raw Supabase do not.
| Endpoint | Events emitted |
|---|---|
GET/POST/PATCH/DELETE /api/items/[collection]/** | ✅ Full CRUD events |
GET/POST/PATCH/DELETE /api/users/** | ✅ Full CRUD events on daas_users |
GET/POST /api/roles/** | ✅ Full CRUD events on daas_roles |
GET/POST /api/policies/** | ✅ Full CRUD events on daas_policies |
GET/POST /api/workflows/** | ✅ Full CRUD events on daas_wf_definition |
GET/POST /api/workflow-assignments/** | ✅ Full CRUD events on daas_wf_assignment |
POST /api/workflow/transition | ✅ ItemsService events + custom workflow action event |
GET /api/activity | ❌ Read-only, no hooks |
GET/POST /api/fields/** | ❌ Raw Supabase — no events |
GET/POST /api/collections/** | ❌ Raw Supabase — no events |
GET/POST /api/relations/** | ❌ Raw Supabase — no events |
GET/PATCH /api/settings | ❌ Raw Supabase — no events |
GET/POST /api/files | ❌ Bypasses emitter |
MCP tools for daas_users, daas_roles, daas_policies, daas_permissions, and daas_files all route through ItemsService and emit the standard item events.
Filter Hooks
Filter hooks run before the operation and can modify the payload. The operation waits for all filter hooks to complete before proceeding.
// extensions/my-extension/index.mjs
export function register(sdk) {
// Global — runs for every collection
sdk.emitter.onFilter('items.create', async (payload, meta, context) => {
return {
...payload,
created_at: new Date().toISOString(),
};
});
// Collection-specific validation
sdk.emitter.onFilter('articles.items.create', async (payload, meta, context) => {
if (!payload.title || payload.title.length < 5) {
throw new Error('Article title must be at least 5 characters');
}
// Auto-generate slug
payload.slug = payload.title.toLowerCase().replace(/\s+/g, '-');
return payload;
});
}Filter hook signature
type FilterHandler<T = unknown> = (
payload: T, // Data being created/updated
meta: Record<string, any>, // { collection, keys, ... }
context: EventContext, // Accountability + services
) => T | Promise<T>;Action Hooks
Action hooks run after the operation completes, asynchronously. They cannot modify the response.
export function register(sdk) {
// Log all item creations
sdk.emitter.onAction('items.create', async (meta, context) => {
console.log('Item created:', meta.collection, meta.key);
});
// Send notification when article published
sdk.emitter.onAction('articles.items.update', async (meta, context) => {
if (meta.payload?.status === 'published') {
await fetch('https://notify.example.com/webhook', {
method: 'POST',
body: JSON.stringify({ article: meta.key }),
});
}
});
}Action hook signature
type ActionHandler = (
meta: Record<string, any>, // { collection, key, payload, ... }
context: EventContext, // Accountability + services
) => void | Promise<void>;Event Context
Both hook types receive an EventContext:
type EventContext = {
schema: SchemaOverview | null;
accountability: {
user: string; // User ID
role: string | null; // Primary role ID
admin: boolean;
app: boolean;
ip?: string; // IP address (optional)
} | null;
}Available services
context.services is available inside runtime extensions (database-stored code). File-based extensions access services via the SDK (createItemsService, createVersionService). See Extensions for both patterns.
// 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: 'Hello' });
await svc.updateOne(id, { status: 'draft' });
await svc.deleteOne(id);
// Schema services
const collections = await context.services.collections();
const fields = await context.services.fields();
const relations = await context.services.relations();
// Files and versions
const files = await context.services.files();
const versions = await context.services.versions();
// 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;
// Custom services
const notifier = await context.services.custom('slack-notify');
// Raw Supabase client (bypasses RLS — use with care)
const { data } = await context.services.supabase.from('table').select('*');
// Safe HTTP (domain-restricted — configure EXTENSION_ALLOWED_DOMAINS)
const res = await context.services.fetch('https://api.example.com/data');Built-in Hooks
The platform registers several built-in action hooks automatically:
- Audit logging — every create/update/delete writes a record to
daas_activity - Workflow automation — creates/updates workflow instances when items match assignment rules