Modules
Software mirrors the organizations that build it. A business has departments and each department has its own language and concerns. Modules let you honor those boundaries in code.
Sales logic lives in the sales module. Inventory logic lives in the inventory module. Each module is a self-contained folder that groups related models, pages, services, and logic together. As the system grows, the boundaries stay legible because the structure enforces them.
Defining a module
Every module needs a module.ts file at its root:
// modules/inventory/module.ts
import { defineModule } from 'rangka';
export default defineModule({
name: 'inventory',
label: 'Inventory',
icon: 'warehouse',
order: 30,
navigation: [
{
section: 'Items',
items: [
{ page: 'inventory.items', label: 'Items', icon: 'box' },
{ page: 'inventory.item-groups', label: 'Item Groups', icon: 'folders' },
],
},
{
section: 'Transactions',
items: [
{ page: 'inventory.stock-entries', label: 'Stock Entries', icon: 'arrow-right-left' },
],
},
],
});This is enough to get a module showing up in the sidebar with its own navigation sections.
Configuration
| Field | Type | Default | Description |
|---|---|---|---|
name | string | — | Unique identifier, used as namespace for models |
label | string | — | Display name in the UI |
description | string | undefined | Module description shown in navigation |
icon | string | undefined | Lucide icon name for the sidebar |
color | string | undefined | Module color for navigation UI |
type | 'internal' | 'external' | undefined | Module type |
order | number | 0 | Sidebar position (lower = higher) |
depends | string[] | undefined | Modules that must load before this one |
scopes | Record<string, ScopeDefinition> | undefined | Filtering dimensions (see Scopes) |
navigation | NavigationSection[] | undefined | Sidebar sections and items |
File layout
A module follows conventions. The framework discovers files by their directory:
modules/sales/
├── module.ts # Required
├── models/
│ ├── customer.ts # defineModel()
│ └── order.ts # defineModel()
├── pages/
│ └── orders.ts # definePage()
├── hooks/
│ └── order.ts # defineHooks()
├── services/
│ ├── pricing.ts # defineService()
│ └── submitOrder.ts # defineService()
├── jobs/
│ └── daily-summary.ts # defineJob()
├── fixtures/
│ └── seed.ts # defineFixture()
└── widgets/
└── PipelineBoard.tsx # defineWidget()Only module.ts is required. Add other directories as you need them.
Namespacing
A model's full name is {module}.{model}. So a model defined in modules/sales/models/order.ts becomes sales.order. All cross-module references use this qualified name:
// In the accounting module, linking to a sales model
fields: {
sales_order: field.link('sales.order'),
}This naming convention keeps references unambiguous, even as the number of modules grows.
Dependencies
When your module references models from another module, declare the dependency:
defineModule({
name: 'accounting',
label: 'Accounting',
depends: ['core', 'sales'],
});This ensures dependent modules load first. Their models, hooks, and services are registered before yours, so references always resolve.
Navigation
Navigation defines the sidebar structure for your module. Each section groups related items:
navigation: [
{
section: 'Transactions',
items: [
{ page: 'accounting.journal-entries', label: 'Journal Entries', icon: 'book' },
{ page: 'accounting.ledger', label: 'General Ledger', icon: 'list' },
],
},
{
section: '_settings',
items: [
{ page: 'accounting.chart-of-accounts', label: 'Chart of Accounts', icon: 'sitemap' },
],
},
],Sections with the _settings prefix place their items under a dedicated Settings area in the sidebar.
Items can reference pages from other modules using the module.page format.
Planned — not yet implemented.
Navigation items are filtered by the user's page permissions at boot time. Sections with no permitted items are hidden entirely.
Scopes
Scopes are filtering dimensions that automatically restrict queries. The most common example: a multi-company setup where users only see data from their active company.
defineModule({
name: 'core',
label: 'Core',
scopes: {
company: {
model: 'core.company',
default: 'user.default_company',
switchable: true,
},
},
});| Field | Type | Default | Description |
|---|---|---|---|
model | string | — | Model that provides scope values |
default | string | — | Dot-path to the user's default value |
switchable | boolean | false | Show a switcher in the sidebar so users can change active scope |
Models opt in with scope: 'core.company' in their definition. Once opted in, all queries are filtered automatically. The user never sees data from a company they have not selected.
Splitting large domains
As a domain grows complex, it often makes sense to break it into multiple modules that collaborate:
modules/
├── hr/ # Core HR (employees, departments)
├── payroll/ # Payroll processing (depends: ['hr'])
├── attendance/ # Time tracking (depends: ['hr'])
└── recruitment/ # Hiring pipeline (depends: ['hr'])Each has its own navigation, models, and pages. They reference each other's models freely through the dependency system.
Third-party modules
Planned — not yet implemented.
Modules can be distributed as npm packages. Install one, and its models, pages, and logic merge into your app automatically on the next restart. No extra configuration beyond npm install.