0001 — Modular plugin architecture
- Status: accepted
- Date: 2026-04-27
- Tags: modules, architecture
Context
DentalPin serves a wide spectrum of dental clinics: solo practices, group clinics, multi-specialty centers, and country-specific compliance regimes (e.g. Veri*Factu in Spain). A monolithic codebase forces every clinic to carry every feature, makes country-specific compliance code mix with generic clinical logic, and turns every uninstall into a risky migration.
The product also relies on an open-source community to extend it (BSL 1.1 → Apache 2.0). Third-party developers need a clear, stable contract for plugging in features without forking the core.
Decision
Build the system as independent modules under backend/app/modules/<name>/, discovered via pyproject.toml entry points (dentalpin.modules), each owning its router, models, migrations, events, permissions, and frontend layer. Inter-module communication is contract-driven:
- Direct service-to-service imports across modules are forbidden unless the target is in
manifest.depends. - The preferred mechanism for cross-module reactions is the event bus (see ADR 0003).
- Cross-module FKs are allowed only against modules in
depends; CI rejects migrations otherwise. - Permissions are namespaced by the registry: a module returns
resource.action, the registry prefixes with the module name (see ADR 0005).
Each module declares its identity, dependencies, install policy, and role-permission seeds in a single manifest dict on the module class.
Consequences
Good
- Clinics install only what they need; uninstall is a real, tested operation (see ADR 0002 and
backend/tests/test_uninstall_roundtrip.py). - Country-specific compliance lives in its own module (
verifactu) without pollutingbilling. - Third parties can ship modules as separate Python packages with their own entry point.
- Each module is a small, agent-friendly unit of work — agents can be briefed on one module without reading the whole codebase.
Bad / accepted trade-offs
- More boilerplate per feature (manifest, branch label, frontend layer, events).
- Event-driven cross-module flows are harder to debug than direct function calls; we accept this in exchange for isolation.
- Refactors that span multiple modules need to respect the contract — no drive-by reorganizations.
Alternatives considered
- Single monolithic FastAPI app. Rejected: no clean uninstall, no third-party extensibility, country-specific code drifts everywhere.
- Microservices. Rejected: ops overhead is wrong for a clinic-scale product; transactional integrity inside a clinic matters more than service-level horizontal scaling.
How to verify the rule still holds
backend/tests/test_module_manifest.pybackend/tests/test_module_manifests_consistency.pybackend/tests/test_manifest_validator.py- CI job
manifest-consistency(.github/workflows/ci.yml)
References
docs/technical/creating-modules.md— module-author contractbackend/app/core/plugins/base.py:21—BaseModulebackend/app/core/plugins/manifest.py— manifest schemabackend/app/core/plugins/loader.py:159—discover_modulesbackend/app/core/plugins/registry.py:12—ModuleRegistry- Root
CLAUDE.md— "Modular architecture" section