Skip to content

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:

  1. Modules can be developed, versioned, and distributed independently (now monorepo for simplicity)
  2. The core compiles and runs without any module activated
  3. Licensing is enforced per-module at boot (BSL for repo, signed JWT for enforcement)
  4. 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:

typescript
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 in router.ts before registering routes (Ed25519 offline, from LICENSE env var)
  • Frontend: React routes gated by the same license check

Single repository:

RepoLicenseVisibility
barateza/neemiasBSL 1.1Public (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:

  1. All modules are bundled into the Worker + SPA
  2. At boot, all plugins self-register via registerPlugin() (side-effect import)
  3. Core iterates getPlugins() and wires hooks (routes, i18n, permissions, stores)
  4. License key (signed JWT, Ed25519) is verified offline by frontend and Worker
  5. Only modules present in the key's entitlements array are active

Consequences — ADR-0014

Benefits:

  • Clean separation: core knows nothing about modules except the Plugin interface
  • 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 in main.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

OptionRejected because
npm packages (public)Would expose BSL code to trivial npm install without license enforcement
Git submodulesFragile in CI; sync issues; poor DX
Runtime plugin marketplaceOver-engineering for MVP; adds network dependency for license checks
Fully closed sourceLoses community adoption; the core's value is being open
Fully open sourceNo 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

Distribuído sob licença MIT.