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 24+, pnpm 12+ (o repositório pina pnpm@12.3.1)
  • Write access to the barateza/neemias monorepo (modules live in modules/packages/)
  • 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.

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 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 the monorepo root (barateza/neemias):

workers/src/index.ts — add one side-effect import (the module's registerPlugin() runs at import time), exactly like @neemias/nucleus:

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

App side — the app consumes registered plugins through getPlugins() from @neemias/plugin-registry (app/src/app/routes.tsx for React routes, app/src/app/utils/i18n.ts for translations). If your module registers React routes or i18n namespaces, make sure it is imported somewhere in the app bundle. Nucleus currently wires only the Worker side — use its import in workers/src/index.ts as the template.

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 ​

Reference ​

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

Distribuído sob licença MIT.