> For the complete documentation index, see [llms.txt](https://voyzu.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://voyzu.gitbook.io/docs/extending-voyzu/add-a-new-module.md).

# Add a new module

The Voyzu Rapid Application Development (RAD) Framework is module centric. All functionality is expressed through modules. This page gives high level step by step guidance to add a new module to Voyzu. Because all modules follow the same pattern, information here is also relevant to developing extensions to module functionality.

This page uses the example of adding a new module that extends the core Inventory Financial management capabilities to offer location / warehouse tracking items functionality. For simplicity only limited functionality is in view here; this is an example to illustrate Voyzu's RAD Framework, it is not a full warehousing module.

Our new module will have the name `inventory-warehousing`

{% hint style="info" %}
The example here adds the example module directly to Voyzu core functionality. If you are developing an extension you may wish to use the /extension folders and a new package name to separate your functionality from core functionality
{% endhint %}

## Create database table structure

Database tables do not map 1:1 to modules so we are free to name the table that will hold data for our new module as we wish. The below sql is an example of the data structure that could be used as the contents of our new table - described as SQL in `/infra/db/warehouse_item_location.sql` .

Note the following features:

* The new table has a foreign key out to the existing voyzu items table. This is because we want to integrate with core Voyzu functionality, not replace it
* The standard audit fields are present, and the audit trigger is present. This adds the foundation of auditing capabilities to the new module
* Voyzu domain values such as `display_name` and `business_code` are used - this ensures key fields have consistent properties such as field length
* `code` here represents the item location code. The convention is that every business item has a human readable code in addition to an auto-incrementing `id` field
* `warehouse_name` is the name of the warehouse. In practice you would want want a separate warehouses table and foreign key out to this table.

```sql
-- ============================================================
-- Warehouse Item Location
-- Example only
-- ============================================================

DROP TABLE IF EXISTS warehouse_item_location CASCADE;

CREATE TABLE warehouse_item_location (
    id                       BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (START WITH 10000),
    company_id               BIGINT NOT NULL,
    inventory_item_id        BIGINT NOT NULL,

    warehouse_name           display_name NOT NULL,
    code                     business_code NOT NULL,
    description              description_text,
    status                   active_status NOT NULL DEFAULT 'ACTIVE',

    creation_date            audit_timestamp,
    creation_actor_type      actor_type,
    creation_user_id         TEXT,
    creation_mutation_id     UUID,

    updated_date             audit_timestamp,
    updated_actor_type       actor_type,
    updated_user_id          TEXT,
    updated_mutation_id      UUID,

    deletion_date            audit_timestamp,
    deletion_actor_type      actor_type,
    deletion_user_id         TEXT,
    deletion_mutation_id     UUID,

    CONSTRAINT fk_warehouse_item_location_company
        FOREIGN KEY (company_id)
        REFERENCES company(id),

    CONSTRAINT fk_warehouse_item_location_inventory_item
        FOREIGN KEY (company_id, inventory_item_id)
        REFERENCES inventory_item(company_id, id),

    CONSTRAINT uq_warehouse_item_location_company_warehouse_code
        UNIQUE (company_id, warehouse_name, code),

    CONSTRAINT uq_warehouse_item_location_company_id
        UNIQUE (company_id, id)
);

DROP TRIGGER IF EXISTS warehouse_item_location_audit_trigger
    ON warehouse_item_location;

CREATE TRIGGER warehouse_item_location_audit_trigger
    BEFORE INSERT OR UPDATE OR DELETE
    ON warehouse_item_location
    FOR EACH ROW
    EXECUTE FUNCTION audit_trigger_fn();

```

## Create module logic including UI and database logic

We create the new module as a new folder `inventory-warehousing` within the `/packages/@voyzu/modules` folder. Although every module contains different business functionality, for consistency all broadly follow the same structure:

<table><thead><tr><th width="154">Folder</th><th width="349">Files</th><th>Contents</th></tr></thead><tbody><tr><td><code>client</code></td><td><code>WarehouseItemLocationsListContent.tsx</code><br><code>WarehouseItemLocationDetail.tsx</code><br><code>WarehouseItemLocationDetailsForm.ts</code><br><code>AddWarehouseItemLocationModal.tsx</code></td><td>Provides the warehousing module’s user interface.<br><br>* List and detail components<br>* Add and edit forms<br>* Client-side display and interaction logic<br>* Uses operation-policy results to guide and disable UI operations</td></tr><tr><td><code>domain</code></td><td><code>operation-policy.ts</code></td><td>Defines pure business-operation rules shared by client and server.<br><br>* One function per meaningful operation<br>* DTO-shaped inputs<br>* Returns all applicable blockers<br>* Performs no database access or mutation</td></tr><tr><td><code>server/api</code></td><td><code>warehouse-item-location.http.handlers.ts</code></td><td>Exposes the module’s HTTP operations.<br><br>* Request handling<br>* Parameter and request-body extraction<br>* Mapping service results to HTTP responses</td></tr><tr><td><code>server/db</code></td><td><code>warehouse-item-location.repo.ts</code><br><code>warehouse-item-location.row.types.ts</code></td><td>Implements database access for warehouse item locations.<br><br>* SQL queries<br>* Persisted row types<br>* Insert, read, update and delete operations</td></tr><tr><td><code>server/lib</code></td><td><code>warehouse-item-location.service.ts</code><br><code>warehouse-item-location.validator.ts</code><br><code>warehouse-item-location.mapper.ts</code></td><td>Orchestrates the module’s server-side behaviour.<br><br>* Exhaustive request validation<br>* Loads records and derived state required by operation policies<br>* Authoritatively enforces operation-policy blockers<br>* Repository coordination and DTO mapping</td></tr><tr><td><code>server/pages</code></td><td><code>WarehouseItemLocationsListPage.tsx</code><br><code>WarehouseItemLocationDetailPage.tsx</code></td><td>Provides server-rendered application pages.<br><br>* Loads data required by each page<br>* Passes prepared data to client components</td></tr><tr><td><code>types</code></td><td><code>warehouse-item-location.dto.ts</code><br><code>warehouse-item-location.request.dto.ts</code></td><td>Defines contracts shared across the module.<br><br>* Warehouse item-location DTO<br>* Create and update request DTOs<br>* Shared response, status and derived-state types</td></tr><tr><td>module root</td><td><code>module.ts</code><br><code>package.json</code><br><code>tsconfig.json</code></td><td>Defines and configures the Voyzu module.<br><br>* Module identity and registration metadata<br>* Package exports, including the domain policy<br>* Package dependencies and TypeScript configuration</td></tr></tbody></table>

### Add an operation policy

Every module with business-operation constraints should define them in `domain/operation-policy.ts`. The policy is the single source of truth used by both the client and server; do not reproduce the same rules independently in UI event handlers and services.

For example, a warehouse location may not be deleted while inventory is assigned to it:

```ts
export interface DeleteWarehouseLocationPolicyInput {
  assignedItemCount: number;
}

export interface OperationBlocker {
  code: string;
  message: string;
}

export function getDeleteWarehouseLocationBlockers(
  input: DeleteWarehouseLocationPolicyInput,
): OperationBlocker[] {
  return input.assignedItemCount > 0
    ? [{
        code: "WAREHOUSE_LOCATION_HAS_ITEMS",
        message: "Move assigned items before deleting this location.",
      }]
    : [];
}
```

Keep policy functions pure. They receive all required current and proposed state, perform no database access, and return every applicable blocker. Use a separate function for each meaningful operation, even when two operations currently have the same rules.

The implementation flow is:

1. Define the supported operation in the API and its writable fields in the request DTO.
2. Validate the request shape in `<entity>.validator.ts`. Its field-validator map must exhaustively satisfy the request DTO.
3. In `<entity>.service.ts`, load the current record and any derived state needed by the policy.
4. Call the operation policy in the service and reject the operation when blockers are returned.
5. Call the same policy from the client to disable controls or explain why an operation is unavailable.
6. Protect structural integrity with repository transactions and database constraints.

The server remains authoritative. Client-side policy use improves the interface but never replaces service-side enforcement. See [Validation and Operation Policy Layers](https://github.com/chrisjameslennon/voyzu/tree/tenant-accounting/docs/public/extending-voyzu/patterns/validation-layers.md) for the responsibilities of each layer and a complete example.

For a deeper drill down into the code that the above files would contain and the various patterns in use, inspect or have your AI agent inspect a few Voyzu CRUD style modules, for example the `company-inventory-items` module. If Voyzu [patterns ](/docs/extending-voyzu/patterns.md)are followed, by creating the above files and functionality we now have a complete self-contained items warehousing module including:

* UI pages
* Persistence logic, including transactional support
* A full set of API routes
* API documentation

## Patterns

The consistent application of patterns is a key to being able to execute rapid, high quality feature development within Voyzu. See [Patterns](/docs/extending-voyzu/patterns.md)

## Wire the module into the Application navigation

With our module created all that remains is to wire the module into the Application's navigation system. For this example we will place a new 'Inventory' menu item on the top navigation bar with a single left navigation item, 'Warehousing'.

### Add top nav menu item

The component that powers the top navigation menu is `apps\web\app\(web)\surface\top-nav\TopNavMenu.tsx` Modify this file to add a new navigation button, and attach the path to our new module, for example `/inventory/warehousing`

### Add new left navigation menu

To `apps\web\app\(web)\surface\left-navs` add a new Left Nav. The Finance Left Nav can be used as an example

### Populate our new Left Nav

Now we have our new top navigation button that loads a new Left Navigation panel, the final step is to register our module with the left navigation. To do this we create a new config file in `apps\web\app\(web)\modules-config` Again the finance module configuration (`finance.ui.config.ts`) can be used as an example to follow.

## Provide Help Documentation

The final step is to provide some documentation for our new module to help guide users. Voyzu help is written as markdown files, located in `docs\public` For our new module we can create a new folder and within is a markdown file within `docs\public\modules-help`

We want to take users to this page when they click on the top right "Help" icon - do to this we link our new module to our new online page - this is done by editing the module metadata / registration file (`module.ts`) and add a node similar to:

```json
  pageRoutes: {
    list: {
      id: "voyzu.item-warehousing.page.list",
      pageTitle: "Itemm Warehousing",
      helpUrl: "modules-help/inventory-warehousing",
    },
```
