Skip to content

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:

typescript
// 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

FieldTypeDefaultDescription
namestringUnique identifier, used as namespace for models
labelstringDisplay name in the UI
descriptionstringundefinedModule description shown in navigation
iconstringundefinedLucide icon name for the sidebar
colorstringundefinedModule color for navigation UI
type'internal' | 'external'undefinedModule type
ordernumber0Sidebar position (lower = higher)
dependsstring[]undefinedModules that must load before this one
scopesRecord<string, ScopeDefinition>undefinedFiltering dimensions (see Scopes)
navigationNavigationSection[]undefinedSidebar 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:

typescript
// 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:

typescript
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 defines the sidebar structure for your module. Each section groups related items:

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

typescript
defineModule({
  name: 'core',
  label: 'Core',
  scopes: {
    company: {
      model: 'core.company',
      default: 'user.default_company',
      switchable: true,
    },
  },
});
FieldTypeDefaultDescription
modelstringModel that provides scope values
defaultstringDot-path to the user's default value
switchablebooleanfalseShow 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.