Skip to content

Creating a New Module

This guide walks through creating a new module for the Neemias ecosystem. Modules extend the core with premium functionality (BSL-licensed) and are activated via license keys.

Prerequisites

  • Node.js 20+, pnpm 9+
  • Access to the barateza/neemias-modules monorepo
  • Understanding of the plugin registry (@neemias/plugin-registry)
  • Read ADR-0014: Open Core Architecture

Overview

A module is a Plugin that self-registers at import time. The core iterates all registered plugins and wires them into the Worker, React app, Dexie DB, i18n, and permission system.

neemias-modules/packages/<module-name>/
├── package.json
├── src/
│   ├── index.ts            # Plugin registration (entry point)
│   ├── routes/             # Worker API handlers
│   ├── pages/              # React components (optional — can stay in core)
│   ├── db/                 # Dexie repositories (optional)
│   ├── services/           # Business logic (optional)
│   ├── i18n/               # Translations (optional)
│   └── schemas.ts           # Module-specific schemas

Step 1 — Create the package

bash
cd barateza/neemias-modules
mkdir -p packages/<module-name>/src/{routes,pages,db,services,i18n}

Create package.json:

json
{
  "name": "@neemias/<module-name>",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "exports": { ".": "./src/index.ts" },
  "peerDependencies": {
    "@neemias/plugin-registry": "*",
    "@neemias/schemas": "*"
  }
}

Step 2 — Define the plugin

Create src/index.ts:

typescript
import { registerPlugin } from "@neemias/plugin-registry";
import type { Plugin } from "@neemias/plugin-registry";

export const myPlugin: Plugin = {
  id: "my-module", // used for license entitlement check
  name: "My Module",
  version: "1.0.0",
  minCoreVersion: "1.0.0",

  registerWorkerRoutes: (route, mw) => {
    route("GET", "/api/v1/my-resource", [mw.requireAuth()], handleList);
    route(
      "POST",
      "/api/v1/my-resource",
      [mw.requireAuth(), mw.requireRole(["ADMIN"])],
      handleCreate,
    );
  },

  registerReactRoutes: () => [{ path: "/my-module", lazy: () => import("./pages/MyPage") }],

  registerI18n: () => ({
    myModule: {
      title: "My Module",
      add: "Add item",
    },
  }),

  registerPermissions: () => ({
    "myModule.manage": ["ADMIN"],
  }),

  registerMigrations: () => [{ version: 1, sql: "CREATE TABLE IF NOT EXISTS my_table (...)" }],

  registerDexieStores: (db) => {
    // Dexie version registration — use the existing DB instance
    // db.version(N).stores({ ...existing, myTable: "..." });
  },
};

registerPlugin(myPlugin);

Step 3 — Implement Worker routes

Worker handlers are pipeline-compatible functions. Vendor parseBody, json, nowISO, and randomUUID in your module (or import from @neemias/schemas).

typescript
// src/routes/myResource.ts
import { HttpError } from "@neemias/schemas";
import type { AuthPrincipal } from "@neemias/schemas";

export async function handleList(
  request: Request,
  env: { DB: D1Database },
  _ctx: ExecutionContext,
  _principal: AuthPrincipal,
  _corrId: string,
): Promise<Response> {
  const db = env.DB;
  const result = await db.prepare("SELECT * FROM my_table").all();
  return json({ items: result.results });
}

Use env.DB directly — do NOT import getDB() from the core. Modules must be self-contained.

Step 4 — Wire into the core

In barateza/neemias:

workers/src/router.ts — add one import:

typescript
import { myPlugin } from "@neemias/<module-name>";
registerPlugin(myPlugin);

app/src/main.tsx — add one import (side-effect for plugin registration):

typescript
import "@neemias/<module-name>";

Both exist already from the nucleus module extraction. Use them as templates.

Step 5 — Test

Core without module

bash
# Module NOT imported → 0 routes registered
pnpm --dir workers test
# Verify: no /api/v1/<module> routes in createRouter() output

Core with module

bash
# Module imported → routes + i18n + permissions registered
# Write a plugin integration test following workers/src/__tests__/router.test.ts

Checklist

  • [ ] Plugin registered with unique id
  • [ ] Worker routes use env.DB, not getDB()
  • [ ] registerWorkerRoutes passes real requireAuth/requireRole middlewares
  • [ ] registerI18n provides pt-BR keys
  • [ ] registerPermissions defines RBAC keys
  • [ ] registerMigrations SQL uses IF NOT EXISTS
  • [ ] registerDexieStores uses existing DB instance
  • [ ] 1 import line in router.ts + 1 in main.tsx
  • [ ] Tests pass: core without module + core with module

Reference

  • Full example: neemias-modules/packages/nucleus/ — first extracted module
  • Plugin interface: neemias/packages/plugin-registry/src/index.ts
  • ADR: neemias/docs/architecture/adr/ADR-0014.md

Distributed under MIT License.