Backend Module
Defining a BackendModule: routes, plugins, hooks.
A BackendModule is a single object that declares everything the framework needs to wire a module into the backend: identity, schema, permissions, routes, public paths, quotas, events, tasks, audit rules, and a Fastify plugin escape hatch.
Shape
export interface BackendModule {
manifest: ModuleManifest;
migrations: string[];
morbacRequirements?: MorbacRequirements;
routes: BackendRouteDefinition[];
publicPaths?: string[];
quotas?: ModuleQuotaDef[];
customRoutes?: ModuleRouteDescriptor[];
resources?: ResourceDef[];
channels?: EventChannelDef[];
tasks?: BackgroundTaskDef[];
auditedTables?: { table: string; resourceType: string; excludeColumns?: string[] }[];
plugin?: unknown;
onResponseHook?: unknown;
requiredSecrets?: { name: string; minLength?: number; description: string }[];
}manifest
Identity for the module. Used by the framework to upsert app.modules, correlate the backend and frontend halves, and key the license claims.
manifest: { id: 'documents', name: 'Documents', version: '1.0.0', requires: [] }requires lists other module ids this module depends on. Boot fails if a required module is not registered.
migrations
Absolute filesystem paths to SQL files the framework will run after the core migrations. Use import.meta.url and fileURLToPath to resolve paths relative to your package:
migrations: [join(__dirname, '../../database/migrations/documents_0001_schema.sql')],Migrations run in sorted basename order across all modules. Use a stable prefix per module (documents_0001_, documents_0002_, ...) to keep them grouped.
morbacRequirements
Activities and views the module needs in the database. Inserted into morbac.activities and morbac.views on boot if missing.
morbacRequirements: {
activities: [
{ name: 'read', description: 'Read access' },
{ name: 'create', description: 'Create access' },
],
views: [
{ name: 'documents', description: 'Documents resource' },
],
}routes
The data routes the module exposes. Each entry becomes a Fastify route that proxies to PostgREST.
routes: [
{ method: 'GET', path: '/documents', resource: 'documents', permission: { activity: 'read', view: 'documents' } },
{ method: 'POST', path: '/documents', resource: 'documents', permission: { activity: 'create', view: 'documents' } },
]permission: null skips the morbac check and only requires authentication. Absent the registry, the method is denied.
publicPaths
Paths that should be exempt from the global authentication preHandler. Use sparingly; the default is deny.
publicPaths: ['/my-module/webhook-callback']quotas
Per-org record caps enforced by a Fastify preHandler. Detailed in Quotas.
quotas: [
{ key: 'records', table: 'app.documents', orgColumn: 'org_id', routePath: '/documents', description: 'Maximum number of documents' },
]customRoutes
Descriptors for routes registered through the plugin escape hatch. The framework uses them to populate the OpenAPI spec.
customRoutes: [
{ method: 'POST', path: '/documents/export', permission: { activity: 'read', view: 'documents' }, summary: 'Export documents' },
]resources
Logical resources the module emits events for, beyond the ones inferred from routes. Used by the SSE bridge and the webhook emitter.
resources: [{ name: 'documents', events: ['created', 'updated', 'deleted'] }]channels
SSE channels clients can subscribe to. Each entry declares the channel name, the morbac permission required to subscribe, and whether subscriptions are org-scoped.
channels: [
{ channel: 'documents.created', permission: { activity: 'read', view: 'documents' }, orgScoped: true },
]tasks
Background tasks scheduled by the framework after migrations complete.
tasks: [
{
id: 'documents.daily-cleanup',
schedule: '0 3 * * *',
systemAccount: 'system-worker@internal',
handler: async ({ db, log }) => { /* ... */ },
},
]systemAccount must be the email of a row in morbac.system_principals. Boot fails if the account does not exist. exclusive defaults to true: a new run is skipped while a previous one is still active.
auditedTables
Tables the framework should enable PostgreSQL-level audit triggers on. The server calls app.enable_audit(table, resourceType, excludeColumns) for each entry after migration.
auditedTables: [
{ table: 'app.documents', resourceType: 'document', excludeColumns: ['updated_at'] },
]plugin
A Fastify plugin invoked inside the module's scope, after the module-enabled gate and the quota hook. Use this when you need behavior the declarative fields cannot express: custom endpoints, validation hooks, multi-step orchestration.
plugin: async function (scope, ctx) {
scope.post('/documents/export', async (req, reply) => { /* ... */ });
}The context provides db, license, moduleId, webhookEmitter, availableEvents, and appConfig.
onResponseHook
An onResponse Fastify hook registered on the root instance. Use this for metrics that should fire on every request, not just module routes.
requiredSecrets
Environment variables the module needs. Validated at boot by the framework's assertSecretsHardened check. The check enforces a minimum length (default 32) and rejects known weak defaults.
requiredSecrets: [
{ name: 'SMTP_PASSWORD', minLength: 16, description: 'SMTP relay password' },
]If a required secret is missing or trivially weak, boot fails with a non-zero exit code.