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

    Full Example

    A complete module from scaffold to running app.

    This page walks through a complete, minimal tasks module: scaffold, schema, permissions, backend route, frontend page, and the two lines you add to your app to wire it all in.

    1. Scaffold

    bash
    crudy module create tasks
    cd tasks

    You get the layout described in Module Anatomy.

    2. Define the schema

    Create database/migrations/tasks_0001_schema.sql in the module package:

    sql
    CREATE TABLE IF NOT EXISTS app.tasks (
        id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        org_id      UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE,
        title       TEXT NOT NULL CHECK (length(title) BETWEEN 1 AND 200),
        done        BOOLEAN NOT NULL DEFAULT FALSE,
        created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
    );
     
    CREATE INDEX IF NOT EXISTS tasks_org_idx ON app.tasks (org_id);

    The framework will run this file after the core migrations because the module declares it under migrations.

    3. Wire the backend module

    src/backend/index.ts:

    ts
    import { fileURLToPath } from 'url';
    import { dirname, join } from 'path';
    import type { BackendModule } from '@crudy/backend';
     
    const __dirname = dirname(fileURLToPath(import.meta.url));
     
    export const TasksBackend: BackendModule = {
        manifest: { id: 'tasks', name: 'Tasks', version: '0.1.0' },
        migrations: [
            join(__dirname, '../../database/migrations/tasks_0001_schema.sql'),
        ],
        morbacRequirements: {
            activities: [
                { name: 'read',   description: 'Read access' },
                { name: 'create', description: 'Create access' },
                { name: 'update', description: 'Update access' },
                { name: 'delete', description: 'Delete access' },
            ],
            views: [
                { name: 'tasks', description: 'Tasks resource' },
            ],
        },
        routes: [
            { method: 'GET',    path: '/tasks', resource: 'tasks', permission: { activity: 'read',   view: 'tasks' } },
            { method: 'POST',   path: '/tasks', resource: 'tasks', permission: { activity: 'create', view: 'tasks' } },
            { method: 'PATCH',  path: '/tasks', resource: 'tasks', permission: { activity: 'update', view: 'tasks' } },
            { method: 'DELETE', path: '/tasks', resource: 'tasks', permission: { activity: 'delete', view: 'tasks' } },
        ],
        quotas: [
            { key: 'records', table: 'app.tasks', orgColumn: 'org_id', routePath: '/tasks', description: 'Maximum number of tasks' },
        ],
        auditedTables: [
            { table: 'app.tasks', resourceType: 'task' },
        ],
    };

    The four routes are proxied to PostgREST against the app.tasks table; the morbac extension applies row-level filtering by org for any user that holds the matching activity-on-view rule.

    4. Wire the frontend module

    src/frontend/index.ts:

    tsx
    import type { FrontendModule, ModulePageProps } from '@crudy/frontend';
    import { ListChecks } from 'lucide-react';
     
    function TaskListPage({ token, effectiveOrgIds, Card, CardHeader, CardTitle, CardContent }: ModulePageProps) {
        const orgId = effectiveOrgIds[0];
        if (!orgId) return null;
        return (
            <Card>
                <CardHeader><CardTitle>Tasks</CardTitle></CardHeader>
                <CardContent>{/* fetch /data/tasks with token + X-Org-Id */}</CardContent>
            </Card>
        );
    }
     
    export const TasksFrontend: FrontendModule = {
        manifest: { id: 'tasks', name: 'Tasks', version: '0.1.0' },
        navItems: [
            { href: '/tasks', label: 'Tasks', icon: ListChecks },
        ],
        routes: [
            { path: '/tasks', Component: TaskListPage },
        ],
    };

    The page uses the host UI primitives (Card, CardHeader, etc.) that arrive via ModulePageProps. That keeps the module visually consistent with whatever app embeds it.

    Tip

    For data calls, always pass credentials: 'include' on fetch, send the bearer token in the Authorization header, and add X-Org-Id for any org-scoped resource.

    5. Get the module added to your license

    A module only runs if your license enables it. Email Crudy or open a request from /pricing with:

    • The audience from your app.config.json.
    • The module id you want enabled (here, tasks).
    • The quota you need per org (here, records).

    We send back a refreshed license.lic. Drop it in place of the existing one and restart the backend.

    Missing module id, the module not enabled, or a missing limit key all block the module. See Quotas for the limit-value rules.

    6. Wire the module into the app

    apps/my-app/backend/server.ts:

    ts
    import { createCrudyServer } from '@crudy/backend';
    import { TasksBackend } from '@crudy-modules/tasks/backend';
    import appConfig from '../app.config.json' with { type: 'json' };
     
    await createCrudyServer({ modules: [TasksBackend], appConfig });

    apps/my-app/frontend/app/providers.tsx:

    tsx
    "use client";
     
    import type { ReactNode } from "react";
    import { CrudyProviders } from "@crudy/frontend";
    import type { AppConfig } from "@crudy/frontend";
    import { TasksFrontend } from "@crudy-modules/tasks/frontend";
    import rawConfig from "../../app.config.json";
     
    const appConfig = rawConfig as AppConfig;
    const MODULES = [TasksFrontend];
     
    export function AppProviders({ children }: { children: ReactNode }) {
        return (
            <CrudyProviders modules={MODULES} appConfig={appConfig}>
                {children}
            </CrudyProviders>
        );
    }

    7. Reset, restart, log in

    bash
    make dev-reset
    make dev-up
    crudy admin reset

    Log in at http://localhost:3001/login, grant a user the create/tasks rule on an org, and the Tasks page appears in the sidebar.

    PreviousQuotasNext Overview
    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