crudy
    FeaturesUse CasesDocumentationPricingAboutContact
    Get started free
    Documentation
    • Introduction
    • Installation
    • Quickstart
    • CLI Reference
    • Overview
    • Core Packages
    • App Structure
    • Modules & Routes
    • Module Anatomy
    • Backend Module
    • Frontend Module
    • Quotas
    • Full Example
    • Overview
    • Docker
    • Environment Variables
    • Licensing
    Modules

    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

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

    ts
    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:

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

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

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

    ts
    publicPaths: ['/my-module/webhook-callback']

    quotas

    Per-org record caps enforced by a Fastify preHandler. Detailed in Quotas.

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

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

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

    ts
    channels: [
        { channel: 'documents.created', permission: { activity: 'read', view: 'documents' }, orgScoped: true },
    ]

    tasks

    Background tasks scheduled by the framework after migrations complete.

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

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

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

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

    PreviousModule AnatomyNext Frontend Module
    crudy

    Build internal tools your teams actually use. Powered by a framework built for developers.

    Product

    • Features
    • Use Cases
    • Pricing

    Company

    • About
    • Contact

    © 2026 Crudy. All rights reserved.

    crudy.fr