Umsetzungsbericht: Modul-Architektur
Dieser Bericht dokumentiert die vollständige Umsetzung der Prilog Modul-Architektur (Handbuch v1.1). Er ersetzt den ursprünglichen Umsetzungsplan und dient als Referenz für jeden Entwickler der am System arbeitet.
Abgeschlossen: 30. März 2026
Repositories
| Repo | Rolle in der Modul-Architektur |
|---|---|
prilog-backend-api | Core: Event-Bus, Lifecycle-Manager, Billing, Monitoring, Store-API, Platform-API |
prilog-web-client | Frontend: Module-Registry, Lazy Loading, dynamische Routen |
prilog-portal | Admin-UI: Store-Seite, Modul-Installation/Deaktivierung |
prilog-sdk | SDK-Monorepo: @prilog/types, @prilog/sdk, @prilog/create-module, @prilog/module-validator |
prilog_docs | Diese Dokumentation |
Phase 0: Core-Infrastruktur
Prisma-Schema
8 neue Tabellen in prilog-backend-api/prisma/schema.prisma:
| Tabelle | Zweck |
|---|---|
module_registrations | Zentrale Registry aller Module (intern + Store) |
tenant_module_installations | Modul-Installation pro Tenant mit Lifecycle-Status |
module_events | Event-Bus-Persistenz (Audit + Replay) |
module_configs | Key-Value-Konfiguration pro Tenant/Modul |
module_usage_events | Billing: Nutzungs-Tracking |
developer_accounts | Entwickler-Accounts für Store-Einreichungen |
store_submissions | Modul-Einreichungs-Pipeline |
store_ratings | Bewertungen (1-5 Sterne + Kommentar) |
Migrationen: 10_add_module_architecture_tables, 11_add_store_tables
Event-Bus
Pfad: src/core/event-bus/
- Redis Pub/Sub, PM2-Cluster-fähig (Pattern-Subscribe auf
prilog:events:*) - Jedes Event wird in
module_eventspersistiert (Audit + Replay) - Handler-Timeout: 5 Sekunden
- Fehler in Handlern werden geloggt, blockieren aber nicht den Bus
9 Core-Events (definiert in core-events.ts):
| Event | Wo emittiert |
|---|---|
user.created | routes/customer/invitations.router.ts (Einladung + Access-Request) |
user.joined | routes/platform-v1.ts (Membership erstellt) |
user.left | routes/platform-v1.ts (Membership gelöscht) |
space.created | routes/platform-v1.ts (Space erstellt) |
space.deleted | routes/platform-v1.ts (Space gelöscht) |
room.created | Noch nicht emittiert (kommt mit Matrix-Connector-Integration) |
module.activated | core/module-lifecycle/lifecycle-manager.ts |
module.deactivated | core/module-lifecycle/lifecycle-manager.ts |
billing.period.end | Noch nicht emittiert (Cron-Job bei Bedarf) |
Alle Events sind fire-and-forget (.catch(() => {})), damit sie den Request nicht blockieren.
Lifecycle-Manager
Pfad: src/core/module-lifecycle/lifecycle-manager.ts
| Funktion | Was sie tut |
|---|---|
installModule(tenantId, moduleId, permissions) | Validiert Manifest-Berechtigungen, erstellt TenantModuleInstallation |
activateModule(tenantId, moduleId) | Status: migrating → active. Ruft module.register(ctx). Emittiert Event. Startet Billing. Loggt Audit. |
deactivateModule(tenantId, moduleId) | Ruft module.unregister(ctx). Entfernt Event-Handler. Setzt 30-Tage-Frist. Stoppt Billing. |
cleanupExpiredModuleData() | Löscht Modul-Daten nach 30 Tagen. Für täglichen Cron. |
getActiveModulesForTenant(tenantId) | Für Bootstrap-Endpunkt. |
Scoped Contexts (scoped-contexts.ts):
Jedes Modul erhält einen isolierten Kontext:
| Context | Implementierung |
|---|---|
ctx.cache | Redis mit Prefix prilog:mod:{moduleId}:{tenantId}: |
ctx.events | EventBus-Wrapper, auto-prefixed {moduleId}. für publizierte Events |
ctx.queue | Stub (inline-Ausführung). Bull-Integration bei Bedarf. |
ctx.billing | Schreibt in module_usage_events. reportUsage(), reportActive(), getUsage(). |
ctx.featureFlags | No-op (Flags werden aus Installation-Status im Bootstrap abgeleitet) |
Modul-Berechtigungs-Middleware
Pfad: src/core/middleware/module-auth.ts
requireModulePermission('users:read') — Fastify preHandler der den X-Prilog-Module-Id Header prüft und gegen die genehmigten Berechtigungen in tenant_module_installations.permissions validiert.
Platform-API Erweiterungen
Pfad: src/routes/platform-v1/module-lifecycle.router.ts
| Endpunkt | Funktion |
|---|---|
GET /modules/registry | Alle registrierten Module (mit Install-Status pro Tenant) |
GET /modules/installed | Installierte Module dieses Tenants |
GET /modules/installed/:moduleId | Einzelnes Modul-Detail |
POST /modules/install | Modul installieren (mit Berechtigungs-Genehmigung) |
POST /modules/installed/:moduleId/activate | Modul aktivieren |
POST /modules/installed/:moduleId/deactivate | Modul deaktivieren |
DELETE /modules/installed/:moduleId | Modul deinstallieren (30-Tage-Frist) |
Bootstrap-Endpunkt (platform-v1.ts): Liefert jetzt featureFlags aus aktiven Modul-Installationen und erweiterte Modul-Informationen (moduleId, apiPrefix).
Phase 2: Projekt-Modul als Proof of Concept
Pfad: src/modules/project/
Das bestehende Projekt-Modul (Files, Boards, Activity) wurde als erstes echtes Handbuch-Modul eingerichtet:
| Datei | Zweck |
|---|---|
prilog-module.json | Manifest (Typ B, 5 Berechtigungen, featureFlag: "project") |
index.ts | register(): Subscribed auf space.created, erstellt Default-Ordner |
index.ts | unregister(): Graceful Deaktivierung |
index.ts | cleanup(): Löscht alle Modul-Daten (S3 + DB) nach 30-Tage-Frist |
Legacy-Kompatibilität: projectModule-Export für registerModules() bleibt erhalten.
Seed-Script: prisma/seed-module-registry.ts — registriert das Modul und erstellt Installationen für alle bestehenden Tenants.
Interner Modul-Loader: lifecycle-manager.ts enthält eine Map internalModules die prilog-project auf den Import-Pfad auflöst. Neue interne Module werden hier eingetragen.
Phase 3: Frontend Modul-Integration
Pfad: prilog-web-client/src/core/module-registry/
Module-Registry
Zentraler Ort wo Frontend-Module definiert werden (module-registry.ts):
const MODULES: FrontendModuleDef[] = [
{
key: 'chat',
routes: [
{ path: 'chat', label: 'Chat', icon: MessageSquare, component: LazyChatPlaceholder },
],
},
{
key: 'project',
routes: [
{ path: 'files', label: 'Dateien', icon: FolderOpen, component: LazyFilesPlaceholder },
{ path: 'tasks', label: 'Aufgaben', icon: CheckSquare, component: LazyTasksPlaceholder },
{ path: 'calendar', label: 'Kalender', icon: Calendar, component: LazyCalendarPlaceholder },
],
},
];Neues Modul hinzufügen (Frontend)
- Komponente erstellen in
src/features/modules/ - Lazy-Import in
module-registry.tshinzufügen - Eintrag in
MODULES-Array
Die SpaceView (features/shell/space-view.tsx) generiert Tabs und Routen dynamisch aus der Registry + Bootstrap-Response. Nur aktivierte Module erscheinen.
Phase 4: SDK
Repo: prilog-sdk/ (Monorepo mit npm workspaces)
@prilog/types
Alle TypeScript-Interfaces: ModuleManifest, PrilogModuleContext, PrilogModule, CoreEventPayloads, ModulePermission, etc.
@prilog/sdk
Re-exportiert @prilog/types + SDK-Utilities:
defineModule(mod)— Type-safe Modul-DefinitionvalidateManifest(json)— Programmatische Manifest-Validierung (ID-Format, SemVer, Berechtigungen, Typ-Constraints, Billing)
@prilog/create-module
CLI-Tool: npx @prilog/create-module mein-modul --type=B
Generiert:
mein-modul/
├── prilog-module.json
├── package.json
├── tsconfig.json
├── src/index.ts
├── src/types.ts
├── src/routes/ (Typ B/C)
├── src/services/ (Typ B/C)
├── src/events/
├── prisma/ (Typ B/C)
├── tests/
├── README.md
└── .gitignore@prilog/module-validator
CLI-Tool: npx prilog-validate ./mein-modul
Prüft:
- Manifest vorhanden und valide
- Pflichtdateien (
src/index.ts,README.md) register()/unregister()Exports- Typ-spezifische Checks (Typ A hat keine Migrations)
- CHANGELOG.md ab v1.0
Installation
Pakete sind als Tarballs in prilog-sdk/dist/ gebaut. Installation:
npm install /pfad/zu/prilog-sdk/dist/prilog-types-1.0.0.tgz
npm install /pfad/zu/prilog-sdk/dist/prilog-sdk-1.0.0.tgzFür GitHub Packages Publish: Token mit write:packages Scope benötigt.
Phase 5: Prilog Store
Store-Backend
Pfad: src/routes/store/
| Datei | Endpunkte |
|---|---|
catalog.router.ts | GET /store/modules (Suche, Filter, Pagination), GET /store/modules/:id (Detail), GET /store/categories, POST /store/modules/:id/ratings |
submission.router.ts | POST /store/developer/submit, GET /store/developer/submissions, GET /store/admin/reviews, POST /store/admin/reviews/:id (approve/reject) |
manifest-validator.ts | Server-seitige Manifest-Validierung (spiegelt SDK-Validator) |
Katalog liefert pro Modul: Name, Beschreibung, Typ, Autor, Preis, Durchschnittsbewertung, Installationszahl, Installations-Status des anfragenden Tenants.
Review-Workflow: pending → scanning → testing → in_review → approved/rejected. Bei Approval wird das Modul automatisch in module_registrations eingetragen.
Store-Portal-UI
Pfad: prilog-portal/src/app/modules/store/page.tsx
- Modul-Katalog mit Suche und Kategorie-Filter
- Modul-Cards: Name, Typ, Autor, Version, Preis, Bewertung, Installationszahl
- Install-Button (installiert + aktiviert in einem Schritt)
- Deactivate-Button für aktive Module
- API-Client-Erweiterungen in
lib/api.ts
Phase 6: Billing
Pfad: src/core/billing/
module-billing.service.ts
| Funktion | Trigger | Was passiert |
|---|---|---|
onModuleActivated() | Lifecycle-Manager | Erstellt Stripe Subscription Item (subscription/pay-per-use) oder Invoice Item (one-time) |
onModuleDeactivated() | Lifecycle-Manager | Entfernt Subscription Item mit Proration |
Erstellt automatisch Stripe Products + Prices aus dem Manifest-Billing-Config. Speichert stripeSubscriptionItemId in der Installation-Config.
usage-aggregator.service.ts
| Funktion | Trigger | Was passiert |
|---|---|---|
processMonthlyUsage(period) | Monatlicher Cron | Aggregiert ModuleUsageEvent-Records, meldet Summe an Stripe als Usage Record |
getUsageSummary(tenantId, moduleId) | Portal-API | Zeigt aktuelle Nutzung für den laufenden Monat |
developer-payout.service.ts
| Funktion | Trigger | Was passiert |
|---|---|---|
processPayouts(period) | Monatlicher Cron (nach Invoice-Finalisierung) | Berechnet 70/30 Split, transferiert via Stripe Connect |
- Minimum-Auszahlung: 50 EUR (darunter wird auf nächsten Monat übertragen)
- Berechnet Revenue aus Subscription-Count, Usage-Aggregation und One-Time-Installationen
Hinweis: Billing-Fehler blockieren nie den Lifecycle — alle Aufrufe sind non-blocking.
Phase 7: Monitoring
Pfad: src/core/monitoring/
module-metrics.ts
Fastify onResponse-Hook der bei jedem Request:
- Prüft ob der Request zu einem Modul gehört (via
X-Prilog-Module-IdHeader oder URL-Pattern) - Response-Zeit in Redis Sorted Set speichert (Rolling Window, 100 Einträge)
- Error-Count (4xx/5xx) trackt
- Bei p95 > 200ms oder p99 > 500ms eine Violation loggt
Performance-Contract-Check: checkPerformanceContract(moduleId) → p95, p99, Error-Rate, Violations-Liste.
Violation-Tracking: Konsekutive Überschreitungen werden in Redis gezählt. Warnung bei 3x p95, bei 10x wird geloggt. Keine automatische Deaktivierung — zu riskant für Schulen.
module-health.ts
getAllModuleHealth() — Dashboard-Daten für alle aktiven Module:
- Name, Version, aktive Installationen
- Performance: p95, p99, Error-Rate, Violations, healthy-Flag
module-audit.ts
Schreibt in die bestehende tenant_events-Tabelle:
module.installed,module.activated,module.deactivatedmodule.uninstalled,module.data_deletedmodule.billing_started,module.billing_stoppedmodule.performance_warningmodule.submission_approved,module.submission_rejected
Admin-API
Pfad: src/routes/admin/module-health.router.ts
| Endpunkt | Funktion |
|---|---|
GET /admin/modules/health | Alle Module — Gesundheitsübersicht |
GET /admin/modules/health/:moduleId | Einzelnes Modul — Detail mit Performance-Daten |
GET /admin/modules/audit/:tenantId | Audit-Log eines Tenants (filterbar nach Modul) |
Verzeichnisstruktur (Ist-Zustand)
prilog-backend-api/src/core/
src/core/
├── event-bus/
│ ├── event-bus.ts ← Singleton EventBus (Redis Pub/Sub + DB)
│ ├── core-events.ts ← 9 Core-Event-Typen mit Payloads
│ └── index.ts
├── module-lifecycle/
│ ├── lifecycle-manager.ts ← install/activate/deactivate/cleanup
│ ├── scoped-contexts.ts ← Cache, EventBus, Queue, Billing, FeatureFlags
│ ├── types.ts ← PrilogModuleContext, ModuleManifest, etc.
│ └── index.ts
├── billing/
│ ├── module-billing.service.ts ← Stripe bei Aktivierung/Deaktivierung
│ ├── usage-aggregator.service.ts ← Monatliche Usage → Stripe
│ ├── developer-payout.service.ts ← 70/30 Split via Stripe Connect
│ └── index.ts
├── monitoring/
│ ├── module-metrics.ts ← Performance-Metriken (Response-Zeit, Errors)
│ ├── module-health.ts ← Health-Dashboard-Daten
│ ├── module-audit.ts ← Audit-Logging in tenant_events
│ └── index.ts
└── middleware/
└── module-auth.ts ← requireModulePermission() preHandlerprilog-sdk/packages/
packages/
├── types/src/
│ ├── manifest.ts ← ModuleManifest Interface
│ ├── permissions.ts ← 11 Berechtigungen + Metadaten
│ ├── events.ts ← CoreEvents + Payloads + PrilogEvent
│ ├── context.ts ← PrilogModuleContext + Sub-Interfaces
│ ├── module.ts ← PrilogModule Interface
│ └── index.ts
├── sdk/src/
│ ├── define-module.ts ← Type-safe Helper
│ ├── validate-manifest.ts ← Manifest-Validierung
│ └── index.ts
├── create-module/src/
│ └── index.ts ← CLI Scaffold-Generator
└── module-validator/src/
├── validate-directory.ts ← Struktur + Manifest-Check
└── cli.ts ← CLI Entry PointOffene Punkte & nächste Schritte
| Thema | Status | Nächster Schritt |
|---|---|---|
room.created Event | Nicht emittiert | Matrix-Connector-Callback integrieren |
billing.period.end Event | Nicht emittiert | Cron-Job einrichten |
ctx.queue | Stub (inline) | Bull-Integration wenn ein Modul async Jobs braucht |
| SDK auf npm publishen | Tarballs gebaut | gh auth refresh -s write:packages → npm publish |
| Prisma-Schema-Isolation | Per Konvention | Scoped Prisma-Client wenn externe Module kommen |
| Module-Upload (S3) | API existiert, S3-Upload fehlt | Multipart-Upload in submission.router.ts |
| Security-Scan Pipeline | Manifest-Validierung existiert | Dependency-Scan + Code-Pattern-Analyse |
| Cron-Jobs | Funktionen existieren, Crons nicht registriert | node-cron oder PM2 Cron-Restart |
| Dev-Sandbox | SDK-Tooling steht | Docker-Compose mit Core-Stub für Modul-Entwickler |
Neues Modul erstellen (Kurzanleitung)
# 1. Modul scaffolden
npx @prilog/create-module mein-modul --type=B
# 2. SDK installieren
cd mein-modul
npm install /pfad/zu/prilog-sdk/dist/prilog-sdk-1.0.0.tgz
# 3. Manifest anpassen
# → prilog-module.json: id, name, description, permissions, featureFlag
# 4. Logik implementieren
# → src/index.ts: register(), unregister(), cleanup()
# 5. Validieren
npx prilog-validate .
# 6. Internes Modul registrieren
# → prilog-backend-api/src/core/module-lifecycle/lifecycle-manager.ts:
# internalModules Map erweitern
# → Frontend: module-registry.ts Eintrag + Lazy-Import
# 7. Externes Modul einreichen
# → POST /api/platform/v1/store/developer/submit