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
  2. The core compiles and runs without any module installed
  3. Licensing is enforced per-module at runtime (BSL for modules, MIT for core)
  4. Adding a new module requires minimal changes to the core

Decision — ADR-0014

Open Core with plugin registry. The core (barateza/neemias, MIT + CLA) exposes a Plugin interface in packages/plugin-registry/. Modules (barateza/neemias-modules, BSL) implement this interface and self-register at import time via registerPlugin().

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 defense-in-depth:

  • Frontend: React routes gated by licenseHas(plugin.id) (IndexedDB-stored license key)
  • Worker: verifyLicenseKey() middleware on every module route (Ed25519 offline check)
  • Dexie: operations gated by the same license check

Repository separation:

RepoLicenseVisibility
barateza/neemiasMIT + CLAPublic
barateza/neemias-modulesBSL 1.1Private (public after MVP release)

The plugin-registry package lives in the MIT repo. Modules live in the BSL monorepo. They are never published to the public npm registry — the code is bundled directly into the Worker + SPA at build time. All modules are shipped in the bundle; license keys activate them at runtime.

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

Distributed under MIT License.