Module Anatomy
Inside a Crudy module: shape, files, lifecycle.
A module is a single npm package with two entry points: one for the backend, one for the frontend. The app wires the backend half into createCrudyServer and the frontend half into CrudyProviders. The framework handles everything in between.
On-disk layout
my-module/
package.json
tsconfig.json
src/
backend/index.ts
frontend/index.tscrudy module create <name> produces exactly this layout.
package.json
The package declares two entry points through the exports map:
{
"name": "@crudy-modules/my-module",
"version": "0.1.0",
"type": "module",
"exports": {
"./backend": "./src/backend/index.ts",
"./frontend": "./src/frontend/index.ts"
},
"peerDependencies": {
"@crudy/backend": "latest",
"@crudy/frontend": "latest"
}
}@crudy/backend and @crudy/frontend are peer dependencies. They are not bundled into the module; the host app provides them. This avoids version skew when multiple modules are installed.
src/backend/index.ts
Exports a single BackendModule constant. The minimum shape:
import type { BackendModule } from '@crudy/backend';
export const MyModuleBackend: BackendModule = {
manifest: { id: 'my-module', name: 'My Module', version: '0.1.0' },
migrations: [],
routes: [],
};The manifest.id is the canonical module identifier. It appears in the license claims, in app.modules, in app.org_modules, and in audit log entries. It must be stable across versions.
src/frontend/index.ts
Exports a single FrontendModule constant:
import type { FrontendModule } from '@crudy/frontend';
export const MyModuleFrontend: FrontendModule = {
manifest: { id: 'my-module', name: 'My Module', version: '0.1.0' },
navItems: [],
routes: [],
settingsTabs: [],
};The two halves must agree on manifest.id. The framework uses it to correlate the backend module's enablement state with the frontend module's UI.
Optional globals.css
A module that ships custom Tailwind layers can include a globals.css and document that the host app imports it from its own app/globals.css:
@import "@crudy/frontend/globals.css";
@import "@crudy-modules/my-module/globals.css";Each package owns its own Tailwind. Do not import tailwindcss in your module unless you also configure the layers correctly; in practice modules consume the framework's host UI primitives and skip styling entirely.
How the two halves connect
Nothing in the package itself glues the halves together. The app imports them separately:
// backend/server.ts
import { MyModuleBackend } from '@crudy-modules/my-module/backend';
await createCrudyServer({ modules: [MyModuleBackend], appConfig });// frontend/app/providers.tsx
import { MyModuleFrontend } from '@crudy-modules/my-module/frontend';
const MODULES = [MyModuleFrontend];The framework treats the matching manifest.id as the contract between the two halves.
Lifecycle
When the backend boots:
- The license is verified.
- The module's
migrationsare applied alongside other modules' migrations. app.modulesandmorbac.activities/morbac.viewsare upserted from the module'smanifestandmorbacRequirements.app.enable_auditis called for each entry inauditedTables.- Routes are registered, gated by enablement and quota preHandlers.
- Background tasks are scheduled.
When the frontend renders:
CrudyProvidersregisters each module's nav items, settings tabs, slot contributions, and routes.- The catch-all route resolves URLs against the registered route paths.
- Permission and license checks run on every render of a non-public page.
Continue with Backend Module for the full field reference.