ADR-0014: Open Core Architecture — Plugin-based Modules
- Status: Accepted
- Date: 2026-07-03
Context — ADR-0014
The project needs a sustainable business model while preserving the open-source core. The target market (churches in Brazil) segments naturally: small churches use the free core; larger churches pay for premium modules (nuclei/cells, check-in, events).
The challenge is how to decouple modules from the core so that:
- Modules can be developed, versioned, and distributed independently (now monorepo for simplicity)
- The core compiles and runs without any module activated
- Licensing is enforced per-module at boot (BSL for repo, signed JWT for enforcement)
- Adding a new module requires minimal changes to the core
Decision — ADR-0014
Open Core with plugin registry. The core exposes a Plugin interface in packages/plugin-registry/. Modules implement this interface and self-register at import time via registerPlugin(). All module code lives in the same monorepo under modules/packages/ — barateza/neemias-modules was de-submoduled and archived. BSL 1.1 covers the entire repository; the signed license file gates runtime activation.
Plugin interface — 6 hooks cover all integration points:
interface Plugin {
id: string; // license entitlement key
name: string; // display name
version: string; // semver
minCoreVersion: string; // compatibility gate
registerDexieStores?: (db) => void; // IndexedDB tables
registerWorkerRoutes?: (route, mw) => void; // API routes + middlewares
registerReactRoutes?: () => Route[]; // React Router paths
registerI18n?: () => Translations; // i18n namespaces
registerPermissions?: () => PermMap; // RBAC keys
registerMigrations?: () => Migration[]; // D1 SQL
}License enforcement is boot-time gating:
- Worker:
isModuleLicensed(plugin.id)check inrouter.tsbefore registering routes (Ed25519 offline, fromLICENSEenv var) - Frontend: React routes gated by the same license check
Single repository:
| Repo | License | Visibility |
|---|---|---|
barateza/neemias | BSL 1.1 | Public (source-available) |
All code lives in one monorepo under BSL. The signed license file (LICENSE env var) gates module activation at boot. No separate module repo, no distribution mechanism, no npm publishing needed.
Module activation flow:
- All modules are bundled into the Worker + SPA
- At boot, all plugins self-register via
registerPlugin()(side-effect import) - Core iterates
getPlugins()and wires hooks (routes, i18n, permissions, stores) - License key (signed JWT, Ed25519) is verified offline by frontend and Worker
- Only modules present in the key's
entitlementsarray are active
Consequences — ADR-0014
Benefits:
- Clean separation: core knows nothing about modules except the
Plugininterface - Independent versioning: modules can release on their own cadence
- Zero configuration for churches: one-click deploy ships all modules
- Licensing is cryptographic (Ed25519 signatures), not trust-based
- Adding a module = 1 import line in
router.ts+ 1 inmain.tsx
Tradeoffs:
- All modules in the bundle increase payload (~50-100 KB per module)
- Module extraction requires discipline: schemas split between core and module
- License gate is checked at multiple layers (frontend + Worker + Dexie) — slight duplication
- Plugin registry is mutable global state (acceptable for serverless; reset in tests)
Alternatives Considered — ADR-0014
| Option | Rejected because |
|---|---|
| npm packages (public) | Would expose BSL code to trivial npm install without license enforcement |
| Git submodules | Fragile in CI; sync issues; poor DX |
| Runtime plugin marketplace | Over-engineering for MVP; adds network dependency for license checks |
| Fully closed source | Loses community adoption; the core's value is being open |
| Fully open source | No revenue path; ChMS market in Brazil is proprietary |
References — ADR-0014
- ../../product/prd.md
- ../sdd.md — Open Core section
- .specs/features/059-open-core-design/spec.md — Full decision record (21 decisions)
- https://github.com/barateza/neemias/issues/156 — Exploratory issue
- CLA.md — Contributor License Agreement
- Requirement IDs: FR-OpenCore, NFR-license, NFR-modularity