Modules & Routes
How modules register pages, nav items, and routes.
A module is the unit of feature you add to a Crudy app. The same package exports a backend half and a frontend half. The app wires the backend half in backend/server.ts and the frontend half in frontend/app/providers.tsx. The framework does the rest: enable/disable gating, license quota enforcement, route mounting, and navigation rendering.
Backend routes
BackendModule.routes is an array of BackendRouteDefinition:
export interface BackendRouteDefinition {
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
path: string;
resource: string;
permission: { activity: string; view: string } | null;
}Each entry maps to a proxied data route. The framework registers the Fastify route at path, forwards to the PostgREST resource named resource, and enforces the morbac permission. A permission of null means any authenticated user; absent the registry, the route is denied.
The full set of routes for a module looks like the one shipped by the documents module:
export const documentRoutes = [
{ method: 'GET', path: '/documents', resource: 'documents', permission: { activity: 'read', view: 'documents' } },
{ method: 'POST', path: '/documents', resource: 'documents', permission: { activity: 'create', view: 'documents' } },
{ method: 'PATCH', path: '/documents', resource: 'documents', permission: { activity: 'update', view: 'documents' } },
{ method: 'DELETE', path: '/documents', resource: 'documents', permission: { activity: 'delete', view: 'documents' } },
];Before any of these routes runs, the framework checks that the module is enabled for the request's X-Org-Id both in app.org_modules and in the license. If either side denies, the request returns 404.
Frontend routes
FrontendModule.routes is an array of RouteDefinition:
export type RouteDefinition =
| { path: string; Component: ComponentType<ModulePageProps>; quota?: { key: string }; public?: false; breadcrumb?: BreadcrumbSegment[] }
| { path: string; Component: ComponentType; public: true; breadcrumb?: BreadcrumbSegment[] };Each entry is mounted under the framework's catch-all app/[...slug]/page.tsx. Resolution is exact-match by path. There are no wildcards. A request to /tasks/123 does not resolve to a route whose path is /tasks; you register both explicitly if you need both.
A non-public route receives ModulePageProps: the current token, the current user, the org list, the effective org filter, a checkPermission helper, and the host UI primitives (Button, Card, AppShell, etc.). The host primitives are how a module respects the app's theme and density without depending on @crudy/ui itself.
A public route receives no props. Use it for unauthenticated pages like marketing or documentation.
Navigation
FrontendModule.navItems declares sidebar items:
export interface NavItemDefinition {
href: string;
label: string;
icon: ComponentType<{ className?: string }>;
id?: string;
hiddenByDefault?: boolean;
divider?: boolean;
}Items show up in the sidebar in module-registration order, alongside the framework's built-in items (organizations, users, roles, settings). The user can hide or reorder items per their preferences; hiddenByDefault ships the item in the hidden bucket by default.
Catch-all resolution
The framework's app/[...slug]/page.tsx reads the URL, looks up the matching route in the module registry, runs an authentication check unless public: true, runs the optional quota gate when quota is set, and renders the route's Component with ModulePageProps. If no module owns the path, the framework renders its 404 page.
Always-present framework pages
A small set of framework-owned pages do not live under any module: the home page, login, settings, organizations, users, roles, audit. They are registered in packages/frontend/src/core-routes.ts and are always available, regardless of which modules an app wires. Modules cannot disable them.