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-modulesmonorepo - 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 schemasStep 1 — Create the package
cd barateza/neemias-modules
mkdir -p packages/<module-name>/src/{routes,pages,db,services,i18n}Create package.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:
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).
// 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:
import { myPlugin } from "@neemias/<module-name>";
registerPlugin(myPlugin);app/src/main.tsx — add one import (side-effect for plugin registration):
import "@neemias/<module-name>";Both exist already from the nucleus module extraction. Use them as templates.
Step 5 — Test
Core without module
# Module NOT imported → 0 routes registered
pnpm --dir workers test
# Verify: no /api/v1/<module> routes in createRouter() outputCore with module
# Module imported → routes + i18n + permissions registered
# Write a plugin integration test following workers/src/__tests__/router.test.tsChecklist
- [ ] Plugin registered with unique
id - [ ] Worker routes use
env.DB, notgetDB() - [ ]
registerWorkerRoutespasses realrequireAuth/requireRolemiddlewares - [ ]
registerI18nprovidespt-BRkeys - [ ]
registerPermissionsdefines RBAC keys - [ ]
registerMigrationsSQL usesIF NOT EXISTS - [ ]
registerDexieStoresuses existing DB instance - [ ] 1 import line in
router.ts+ 1 inmain.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