Skip to content

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

RepoRolle in der Modul-Architektur
prilog-backend-apiCore: Event-Bus, Lifecycle-Manager, Billing, Monitoring, Store-API, Platform-API
prilog-web-clientFrontend: Module-Registry, Lazy Loading, dynamische Routen
prilog-portalAdmin-UI: Store-Seite, Modul-Installation/Deaktivierung
prilog-sdkSDK-Monorepo: @prilog/types, @prilog/sdk, @prilog/create-module, @prilog/module-validator
prilog_docsDiese Dokumentation

Phase 0: Core-Infrastruktur

Prisma-Schema

8 neue Tabellen in prilog-backend-api/prisma/schema.prisma:

TabelleZweck
module_registrationsZentrale Registry aller Module (intern + Store)
tenant_module_installationsModul-Installation pro Tenant mit Lifecycle-Status
module_eventsEvent-Bus-Persistenz (Audit + Replay)
module_configsKey-Value-Konfiguration pro Tenant/Modul
module_usage_eventsBilling: Nutzungs-Tracking
developer_accountsEntwickler-Accounts für Store-Einreichungen
store_submissionsModul-Einreichungs-Pipeline
store_ratingsBewertungen (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_events persistiert (Audit + Replay)
  • Handler-Timeout: 5 Sekunden
  • Fehler in Handlern werden geloggt, blockieren aber nicht den Bus

9 Core-Events (definiert in core-events.ts):

EventWo emittiert
user.createdroutes/customer/invitations.router.ts (Einladung + Access-Request)
user.joinedroutes/platform-v1.ts (Membership erstellt)
user.leftroutes/platform-v1.ts (Membership gelöscht)
space.createdroutes/platform-v1.ts (Space erstellt)
space.deletedroutes/platform-v1.ts (Space gelöscht)
room.createdNoch nicht emittiert (kommt mit Matrix-Connector-Integration)
module.activatedcore/module-lifecycle/lifecycle-manager.ts
module.deactivatedcore/module-lifecycle/lifecycle-manager.ts
billing.period.endNoch 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

FunktionWas 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:

ContextImplementierung
ctx.cacheRedis mit Prefix prilog:mod:{moduleId}:{tenantId}:
ctx.eventsEventBus-Wrapper, auto-prefixed {moduleId}. für publizierte Events
ctx.queueStub (inline-Ausführung). Bull-Integration bei Bedarf.
ctx.billingSchreibt in module_usage_events. reportUsage(), reportActive(), getUsage().
ctx.featureFlagsNo-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

EndpunktFunktion
GET /modules/registryAlle registrierten Module (mit Install-Status pro Tenant)
GET /modules/installedInstallierte Module dieses Tenants
GET /modules/installed/:moduleIdEinzelnes Modul-Detail
POST /modules/installModul installieren (mit Berechtigungs-Genehmigung)
POST /modules/installed/:moduleId/activateModul aktivieren
POST /modules/installed/:moduleId/deactivateModul deaktivieren
DELETE /modules/installed/:moduleIdModul 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:

DateiZweck
prilog-module.jsonManifest (Typ B, 5 Berechtigungen, featureFlag: "project")
index.tsregister(): Subscribed auf space.created, erstellt Default-Ordner
index.tsunregister(): Graceful Deaktivierung
index.tscleanup(): 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):

typescript
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)

  1. Komponente erstellen in src/features/modules/
  2. Lazy-Import in module-registry.ts hinzufügen
  3. 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-Definition
  • validateManifest(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:

bash
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.tgz

Für GitHub Packages Publish: Token mit write:packages Scope benötigt.


Phase 5: Prilog Store

Store-Backend

Pfad: src/routes/store/

DateiEndpunkte
catalog.router.tsGET /store/modules (Suche, Filter, Pagination), GET /store/modules/:id (Detail), GET /store/categories, POST /store/modules/:id/ratings
submission.router.tsPOST /store/developer/submit, GET /store/developer/submissions, GET /store/admin/reviews, POST /store/admin/reviews/:id (approve/reject)
manifest-validator.tsServer-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

FunktionTriggerWas passiert
onModuleActivated()Lifecycle-ManagerErstellt Stripe Subscription Item (subscription/pay-per-use) oder Invoice Item (one-time)
onModuleDeactivated()Lifecycle-ManagerEntfernt Subscription Item mit Proration

Erstellt automatisch Stripe Products + Prices aus dem Manifest-Billing-Config. Speichert stripeSubscriptionItemId in der Installation-Config.

usage-aggregator.service.ts

FunktionTriggerWas passiert
processMonthlyUsage(period)Monatlicher CronAggregiert ModuleUsageEvent-Records, meldet Summe an Stripe als Usage Record
getUsageSummary(tenantId, moduleId)Portal-APIZeigt aktuelle Nutzung für den laufenden Monat

developer-payout.service.ts

FunktionTriggerWas 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:

  1. Prüft ob der Request zu einem Modul gehört (via X-Prilog-Module-Id Header oder URL-Pattern)
  2. Response-Zeit in Redis Sorted Set speichert (Rolling Window, 100 Einträge)
  3. Error-Count (4xx/5xx) trackt
  4. 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.deactivated
  • module.uninstalled, module.data_deleted
  • module.billing_started, module.billing_stopped
  • module.performance_warning
  • module.submission_approved, module.submission_rejected

Admin-API

Pfad: src/routes/admin/module-health.router.ts

EndpunktFunktion
GET /admin/modules/healthAlle Module — Gesundheitsübersicht
GET /admin/modules/health/:moduleIdEinzelnes Modul — Detail mit Performance-Daten
GET /admin/modules/audit/:tenantIdAudit-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() preHandler

prilog-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 Point

Offene Punkte & nächste Schritte

ThemaStatusNächster Schritt
room.created EventNicht emittiertMatrix-Connector-Callback integrieren
billing.period.end EventNicht emittiertCron-Job einrichten
ctx.queueStub (inline)Bull-Integration wenn ein Modul async Jobs braucht
SDK auf npm publishenTarballs gebautgh auth refresh -s write:packagesnpm publish
Prisma-Schema-IsolationPer KonventionScoped Prisma-Client wenn externe Module kommen
Module-Upload (S3)API existiert, S3-Upload fehltMultipart-Upload in submission.router.ts
Security-Scan PipelineManifest-Validierung existiertDependency-Scan + Code-Pattern-Analyse
Cron-JobsFunktionen existieren, Crons nicht registriertnode-cron oder PM2 Cron-Restart
Dev-SandboxSDK-Tooling stehtDocker-Compose mit Core-Stub für Modul-Entwickler

Neues Modul erstellen (Kurzanleitung)

bash
# 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