Zum Inhalt

Single-Tenant Multi-Instance Architektur - easySale

Konzept: Single-Tenant mit Shared Codebase
Datum: 24. Februar 2026
Version: 2.0 — Multi-Repo-Architektur
Status: Implementiert

📦 Update März 2026 — Multi-Repo-Architektur

Das Projekt verwendet jetzt separate Git-Repos für Core und Clients: - Core-Repo (easysale-core): Enthält ERP Base, Shop Base, Shared, Functions - Client-Repos (easysale-client-<slug>): Je ein eigenes Repo pro Kunde - Clients referenzieren Core per Git-Dependency in pubspec.yaml - VS Code Multi-Root Workspace mit Core read-only für sichere Entwicklung - Auto-Deployment per GitHub Actions (Core-Push → Client-Rebuild)

Die Grundprinzipien (Single-Tenant, Extensions, ClientConfig) bleiben gleich. Nur die Repository-Struktur und Dependency-Auflösung haben sich geändert.

⚠️ Wichtig: Jeder Kunde erhält eine komplett eigenständige Instanz (Single-Tenant).
Der gemeinsame Code wird nur zur Entwicklung und Wartung geteilt, nicht zur Laufzeit.

📋 Inhaltsverzeichnis

  1. Executive Summary
  2. Problemstellung & Ziele
  3. Lösungsansatz: Single-Tenant mit Shared Codebase
  4. Architektur-Übersicht
  5. Build & Deployment Process
  6. Erweiterungsmöglichkeiten
  7. Workflows & Szenarien
  8. Implementierungsplan
  9. Vor- & Nachteile
  10. Migration & Rollout

Executive Summary

🎯 Das Konzept in 30 Sekunden

Problem:
Jeder Kunde braucht eigene, isolierte App-Instanz. Aber: Bugfixes sollen nicht 10× manuell gemerged werden müssen.

Lösung:
Single-Tenant mit Shared Codebase

┌─────────────────────────────────┐
│   Ein Git Repository            │
│   80% gemeinsamer Core-Code     │
│   20% kundenspezifisch          │
└───────────┬─────────────────────┘
            │ Build & Deploy
┌───────────────────────────────────────────────┐
│  Kunde A          Kunde B          Kunde C    │
│  (Single-Tenant)  (Single-Tenant)  (...)      │
│  ├─ Firebase A    ├─ Firebase B    ├─ Fireba │
│  ├─ App A         ├─ App B         ├─ App C  │
│  └─ Daten A       └─ Daten B       └─ Daten C│
│     (isoliert)       (isoliert)       (...)   │
└───────────────────────────────────────────────┘

Key Facts: - ✅ Single-Tenant: Jeder Kunde = Eigene Firebase + Eigene App + Eigene Daten - ✅ Multi-Repo: Core + separate Client-Repos (sichere Isolation) - ✅ Shared Code: 80% gemeinsam entwickelt & gewartet (per Git-Dependency) - ✅ Bugfix: 1× im Core fixen → automatisch für alle deploybar - ✅ Anpassbar: 20% kundenspezifisch über Extensions - ✅ Sicher: Totale Datenisolation (DSGVO-konform) + Core read-only


📋 Inhaltsverzeichnis

Problemstellung & Ziele

Aktueller Bedarf

  • Pro Kunde eine eigenständige Instanz (ERP + Shop) - SINGLE-TENANT
  • Jeder Kunde hat komplett eigene Firebase-Instanz (eigene Datenbank, Auth, Functions)
  • Jeder Kunde hat eigene Apple/Google-Umgebung (Bundle IDs, Zertifikate)
  • Jeder Kunde wird separat deployed (unabhängige Releases)
  • Datenisolation ist garantiert (keine gemeinsame Datenbank)
  • Individuelle Kundenwünsche sollen möglich sein
  • Bugfixes zentral für alle Kunden (trotz separater Deployments)

Herausforderungen

Branch-per-Client Ansatz:

main
├── client-a-branch
├── client-b-branch  
└── client-c-branch

Probleme: - Bugfixes müssen in JEDEN Branch cherry-picked werden - Nach 6 Monaten divergieren die Branches - Fehleranfällig (Bugfix vergessen?) - Kein Code Reuse - Testing-Aufwand multipliziert sich

Ziele der Lösung

Bugfixes zentral - 1x fixen, alle profitieren
Individualisierbar - Kunden können Anpassungen bekommen
Wartbar - Klare Trennung Core vs. Client-Spezifisch
Skalierbar - 100 Kunden möglich
Schnell aufsetzen - Neuer Kunde in < 1 Stunde


Lösungsansatz: Single-Tenant mit Shared Codebase

Grundprinzip: Isolation + Code-Sharing

Single-Tenant Deployment: - Jeder Kunde = Eigene Firebase-Projekt - Jeder Kunde = Eigene App-Deployment - Jeder Kunde = Eigene Datenbank (komplett isoliert) - Jeder Kunde = Eigene URLs, Domains, Zertifikate

Multi-Repo Shared Codebase Development: - 80% gemeinsamer Code → Im Core-Repo (entwickelt & gewartet) - 20% kundenspezifisch → In separaten Client-Repos (Extensions) - Build-Zeit: Code wird per Git-Dependency kombiniert - Deploy-Zeit: Jeder Kunde bekommt eigene Instanz

┌─────────────────────────────────────┐
│         Core Packages               │
│  (Shared Business Logic & Models)   │
│                                     │
│  - erp_core                         │
│  - shop_core                        │
│  - shared                           │
└─────────────────┬───────────────────┘
                  │ extends/customizes
┌─────────────────────────────────────┐
│       Client Overlays               │
│   (Customer-Specific Extensions)    │
│                                     │
│  clients/                           │
│    ├── client_a/  (80% re-use)     │
│    ├── client_b/  (80% re-use)     │
│    └── client_x_custom/ (Fork)     │
└─────────────────────────────────────┘

Hybrid-Ansatz

Standard-Kunden (80%): - Nutzen Core + Config + Extensions - Feature Flags, Theme-Anpassungen - Kleine UI/Logic-Erweiterungen

Special-Kunden (20%): - Bekommen eigenen Fork wenn nötig - Für komplett andere Anforderungen - Bugfixes manuell mergen (nur für diese)


Architektur-Übersicht

Single-Tenant Deployment-Modell

Jeder Kunde erhält:

Kunde: Pharma AG
├── Firebase Projekt: "pharma-ag-prod"
│   ├── Firestore Database (eigene Daten)
│   ├── Authentication (eigene Users)
│   ├── Cloud Functions (eigener Code)
│   ├── Hosting: https://pharma-ag.web.app
│   └── Storage (eigene Dateien)
├── iOS App
│   ├── Bundle ID: com.pharmaag.erp
│   ├── App Store: Eigener Account
│   └── Push Certificates: Eigene APNs
└── Android App
    ├── Package: com.pharmaag.erp
    ├── Google Play: Eigener Account
    └── FCM: Eigene Konfiguration

Kunde: Baumarkt GmbH
├── Firebase Projekt: "baumarkt-gmbh-prod"
│   ├── Firestore Database (EIGENE Daten - isoliert)
│   ├── Authentication (EIGENE Users - isoliert)
│   └── ...
└── ...

🔒 Totale Datenisolation: - Pharma AG kann NIEMALS Daten von Baumarkt sehen - Separate Firebase-Projekte = separate Datenbanken - Keine tenant_id Filter notwendig - DSGVO-konform durch physische Trennung

Verzeichnisstruktur

# Core-Repo (easysale-core):
easysale-core/
├── core/
│   ├── apps/
│   │   ├── erp_system/              # ERP Base App
│   │   └── shop_system/             # Shop Base App
│   ├── shared/                      # Basis Models, Extensions, Utils
│   ├── functions/                   # Cloud Functions
│   └── docs/                        # Dokumentation
├── onboarding/
│   ├── create_client.sh             # Neuen Client anlegen
│   ├── scripts/                     # Deploy-Skripte
│   └── templates/                   # Workflow-Templates
├── .github/
│   ├── workflows/notify-clients.yml # Auto-Notify bei Core-Push
│   └── client-registry.json         # Welche Clients nutzen welche Version
└── melos.yaml                       # Monorepo Config

# Client-Repos (je eigenes Git-Repo):
easysale-client-pharma/
├── erp/                             # Dünner Wrapper
│   ├── lib/
│   │   ├── main.dart                # Entry Point (registriert ClientConfig)
│   │   ├── config/                  # Client Config
│   │   ├── extensions/              # Model Extensions
│   │   ├── blocs/                   # Custom BLoCs
│   │   └── pages/                   # Custom UI
│   ├── assets/                      # Client Assets
│   └── pubspec.yaml                 # Git-Dependency auf Core
├── firebase/                        # Firebase Config
│   ├── .firebaserc
│   └── firebase.json
├── .github/workflows/auto-deploy.yml
├── .code-workspace                  # Multi-Root Workspace (Core read-only)
└── README.md

easysale-client-baumarkt/
└── ...                              # Gleiche Struktur

Build & Deployment Process

Von Shared Code zu separaten Instanzen

Development Time (Multi-Repo Shared Codebase):

# Core-Repo (read-only für Client-Entwicklung):
easysale-core/
├── core/apps/erp_system/   # Gemeinsamer Code
└── core/shared/            # Gemeinsame Models

# Client-Repos (eigene Git-Repos):
easysale-client-pharma/erp/    # + Pharma Extensions (git: dep auf Core)
easysale-client-baumarkt/erp/  # + Baumarkt Extensions (git: dep auf Core)

Build Time (Code-Kombinierung per Git-Dependency):

# Für Pharma AG
cd easysale-client-pharma/erp
flutter pub get   # Holt Core per Git-Dependency
flutter build web --release

# Ergebnis: build/web/
# = erp_system + shared + pharma_extensions
# → Alles in EINEM Bundle

Deploy Time (Separate Instanzen):

# Pharma AG Deployment (im Client-Repo)
cd easysale-client-pharma/firebase
firebase deploy --project pharma-ag-prod --only hosting
# → https://pharma-ag.web.app

# Baumarkt Deployment (anderes Client-Repo)
cd easysale-client-baumarkt/firebase
firebase deploy --project baumarkt-gmbh-prod --only hosting
# → https://baumarkt-gmbh.web.app

# Oder: Automatisch per GitHub Actions (empfohlen)
# Push auf Core → notify-clients.yml → auto-deploy.yml pro Client

Runtime (Komplette Isolation):

┌─────────────────────────────────┐
│ pharma-ag.web.app               │
│ ↓                               │
│ Firebase: "pharma-ag-prod"      │
│   Firestore: Pharma Daten       │
│   Auth: Pharma Users            │
└─────────────────────────────────┘
        ↕ KEINE Verbindung
┌─────────────────────────────────┐
│ baumarkt-gmbh.web.app           │
│ ↓                               │
│ Firebase: "baumarkt-gmbh-prod"  │
│   Firestore: Baumarkt Daten     │
│   Auth: Baumarkt Users          │
└─────────────────────────────────┘

🎯 Wichtig zu verstehen: - Code teilen (Development) ≠ Daten teilen (Runtime) - Jeder Build ist eigenständig und vollständig - Keine Laufzeit-Abhängigkeiten zwischen Kunden - Wenn Pharma-AG down ist → Baumarkt läuft weiter


Erweiterungsmöglichkeiten

1. 📦 Model Extensions (customData)

Im Core: Model vorbereiten

// packages/shared/lib/models/article/article.dart
class Article extends EntityBase {
  final String number;
  final String name;
  // ... Standard-Felder

  /// Client-spezifische Erweiterungen
  final Map<String, dynamic>? customData;

  Article({
    required this.number,
    required this.name,
    this.customData,
    // ...
  });
}

Im Client: Extension nutzen

// easysale-client-pharma/erp/lib/extensions/article_pharma_extension.dart
class ArticlePharmaData {
  final String chargennummer;
  final DateTime? verfallsdatum;
  final String? zulassungsnummer;

  factory ArticlePharmaData.fromMap(Map<String, dynamic> json) { ... }
  Map<String, dynamic> toMap() { ... }
}

extension ArticlePharmaExtension on Article {
  ArticlePharmaData? get pharmaData {
    final data = customData?['pharma'];
    return data != null ? ArticlePharmaData.fromMap(data) : null;
  }
}

Verwendung:

// Client Code
final article = context.read<ArticlesBloc>().selectedArticle;
final chargennummer = article.pharmaData?.chargennummer ?? 'N/A';


2. 🎨 UI Overrides (Komponenten ersetzen)

Im Core: UI Override Service

// packages/erp_core/lib/services/ui_override_service.dart
class UiOverrideService {
  final Map<Type, WidgetBuilder> _screenOverrides = {};
  final Map<String, WidgetBuilder> _componentOverrides = {};

  /// Screen komplett ersetzen
  void registerScreenOverride<T>(Widget Function(BuildContext) builder) {
    _screenOverrides[T] = builder;
  }

  /// Komponente ersetzen (z.B. Header, List Item)
  void registerComponentOverride(String key, WidgetBuilder builder) {
    _componentOverrides[key] = builder;
  }

  Widget buildScreen<T>(BuildContext context, Widget defaultScreen) {
    return _screenOverrides[T]?.call(context) ?? defaultScreen;
  }

  Widget? buildComponent(String key, BuildContext context) {
    return _componentOverrides[key]?.call(context);
  }
}

Im Core: Template Page anpassbar machen

// apps/erp_template/lib/pages/articles/detail_page.dart
class ArticleDetailPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final uiOverride = context.read<UiOverrideService?>();

    return Scaffold(
      body: Column(
        children: [
          // Header überschreibbar
          uiOverride?.buildComponent('article_detail_header', context) 
            ?? EsDetailPageHeader(article: article),

          // Content
          _buildContent(),
        ],
      ),
    );
  }
}

Im Client: UI anpassen

// easysale-client-pharma/erp/lib/config/pharma_ui_config.dart
class PharmaUiConfig {
  static void configure(UiOverrideService service) {
    // Komplette Page ersetzen
    service.registerScreenOverride<ArticleDetailPage>(
      (context) => PharmaArticleDetailPage(),
    );

    // Oder nur Komponente ersetzen
    service.registerComponentOverride(
      'article_detail_header',
      (context) => PharmaCustomHeader(),
    );

    service.registerComponentOverride(
      'article_list_item',
      (context) => PharmaArticleListItem(),
    );
  }
}


3. 🎯 BLoC Extensions (Business Logic)

3 Varianten:

Variant A: BLoC erweitern (Vererbung)

// easysale-client-pharma/erp/lib/blocs/pharma_articles_bloc.dart

// NEUE Events
class ValidateChargennummer extends ArticleEvent {
  final String chargennummer;
  ValidateChargennummer(this.chargennummer);
}

// NEUE States
class ChargennummerValidated extends BlocLoaded {
  final bool isValid;
  final String? errorMessage;
  ChargennummerValidated({required this.isValid, this.errorMessage});
}

// ERWEITETER BLoC
class PharmaArticlesBloc extends ArticlesBloc {

  PharmaArticlesBloc({
    required super.articleFirebaseService,
    required super.customerFirebaseService,
  }) {
    // Zusätzliche Event Handler
    on<ValidateChargennummer>(_onValidateChargennummer);
  }

  // Originale Methode überschreiben
  @override
  Future<void> _onCreateArticle(
    CreateArticle event,
    Emitter<BaseBlocState> emit,
  ) async {
    // ZUSÄTZLICHE Validierung
    final pharmaData = event.article.customData?['pharma'];
    if (pharmaData == null) {
      emit(BlocOperationFailed('Pharma-Daten erforderlich!'));
      return;
    }

    // Check: Chargennummer bereits vorhanden?
    final exists = await _findByChargennummer(pharmaData['chargennummer']);
    if (exists != null) {
      emit(BlocOperationFailed('Chargennummer bereits vergeben!'));
      return;
    }

    // Original Methode aufrufen
    await super._onCreateArticle(event, emit);

    // Zusätzliche Aktionen
    await _notifyRegulatoryDepartment(event.article);
  }

  // Neue Event Handler
  Future<void> _onValidateChargennummer(
    ValidateChargennummer event,
    Emitter<BaseBlocState> emit,
  ) async {
    final exists = await _findByChargennummer(event.chargennummer);
    emit(ChargennummerValidated(
      isValid: exists == null,
      errorMessage: exists != null ? 'Bereits vergeben' : null,
    ));
  }
}

Variant B: Komplett neuer BLoC (zusätzlich)

// easysale-client-baumarkt/erp/lib/blocs/warehouse_bloc.dart

// Neues Feature: Lagerverwaltung
class WarehouseBloc extends Bloc<WarehouseEvent, WarehouseState> {
  final WarehouseRepository warehouseRepo;

  WarehouseBloc({required this.warehouseRepo}) : super(WarehouseInitial()) {
    on<LoadWarehouseStock>(_onLoadWarehouseStock);
    on<UpdateShelfLocation>(_onUpdateShelfLocation);
  }

  // Komplett eigene Logik
  Future<void> _onLoadWarehouseStock(...) async { ... }
}

Variant C: BLoC Factory Pattern

// packages/erp_core/lib/blocs/bloc_factory.dart
abstract class BlocFactory {
  ArticlesBloc createArticlesBloc({...});
  OrderBloc createOrderBloc({...});
}

class DefaultBlocFactory implements BlocFactory {
  @override
  ArticlesBloc createArticlesBloc({...}) {
    return ArticlesBloc(...);
  }
}

// Client Factory
class PharmaBlocFactory implements BlocFactory {
  @override
  ArticlesBloc createArticlesBloc({...}) {
    return PharmaArticlesBloc(...); // Eigener BLoC
  }
}

4. ⚙️ Service Extensions

Im Core: Service Interfaces

// packages/erp_core/lib/services/article_service.dart
abstract class ArticleService {
  Future<void> saveArticle(Article article);
  ValidationResult validateArticle(Article article);
  double calculatePrice(Article article, Customer customer);
}

class DefaultArticleService implements ArticleService {
  @override
  Future<void> saveArticle(Article article) async {
    await FirebaseFirestore.instance
        .collection('articles')
        .doc(article.id)
        .set(article.toMap());
  }

  @override
  ValidationResult validateArticle(Article article) {
    if (article.number.isEmpty) {
      return ValidationResult.error('Artikelnummer fehlt');
    }
    return ValidationResult.ok();
  }

  @override
  double calculatePrice(Article article, Customer customer) {
    return article.basePrice;
  }
}

Im Client: Service überschreiben

// easysale-client-pharma/erp/lib/services/pharma_article_service.dart
class PharmaArticleService extends DefaultArticleService {

  @override
  Future<void> saveArticle(Article article) async {
    // Zusätzliche Pharma-Validierung
    final pharmaData = article.customData?['pharma'];
    if (pharmaData == null) {
      throw Exception('Pharma-Daten fehlen!');
    }

    // Zusätzliche Collections
    final batch = FirebaseFirestore.instance.batch();

    batch.set(
      FirebaseFirestore.instance.collection('articles').doc(article.id),
      article.toMap(),
    );

    // Extra: Pharma-spezifisch
    batch.set(
      FirebaseFirestore.instance.collection('pharma_articles').doc(article.id),
      {
        'chargennummer': pharmaData['chargennummer'],
        'verfallsdatum': pharmaData['verfallsdatum'],
        'regulatoryStatus': 'APPROVED',
      },
    );

    await batch.commit();
  }

  @override
  ValidationResult validateArticle(Article article) {
    // Original Validierung
    final baseResult = super.validateArticle(article);
    if (!baseResult.isValid) return baseResult;

    // Pharma-spezifische Validierung
    final pharmaData = article.customData?['pharma'];
    if (pharmaData?['chargennummer']?.isEmpty ?? true) {
      return ValidationResult.error('Chargennummer erforderlich');
    }

    return ValidationResult.ok();
  }

  @override
  double calculatePrice(Article article, Customer customer) {
    final basePrice = super.calculatePrice(article, customer);

    // 15% Aufschlag für apothekenpflichtige Artikel
    final pharmaData = article.customData?['pharma'];
    final hasApothekenzulassung = pharmaData?['apothekenzulassung'] == true;

    return hasApothekenzulassung ? basePrice * 1.15 : basePrice;
  }
}


5. 🔌 Plugin System (komplette Feature-Module)

// easysale-client-automotive/erp/lib/plugins/automotive_plugin.dart
class AutomotivePlugin {

  // Eigene Models registrieren
  static void registerModels() { ... }

  // Eigene Services
  static void registerServices(GetIt di) {
    di.registerSingleton<VehicleCompatibilityService>(...);
    di.registerSingleton<TireSeasonService>(...);
  }

  // Eigene UI
  static void registerUI(UiOverrideService ui) {
    ui.registerComponentOverride('article_list_item', 
      (ctx) => AutomotiveArticleListItem());
    ui.registerComponentOverride('article_additional_tabs', 
      (ctx) => VehicleCompatibilityTab());
  }

  // Eigene Routes
  static void registerRoutes(Router router) {
    router.addRoute('/vehicles', (ctx) => VehicleSearchPage());
    router.addRoute('/tire-configurator', (ctx) => TireConfiguratorPage());
  }

  // Alles initialisieren
  static void initialize(GetIt di, UiOverrideService ui, Router router) {
    registerModels();
    registerServices(di);
    registerUI(ui);
    registerRoutes(router);
  }
}

6. 🎨 Theme & Styling

Im Client: Eigenes Theme

// easysale-client-pharma/erp/lib/config/pharma_theme.dart
class PharmaTheme {
  static ThemeData get lightTheme {
    return ThemeData(
      primaryColor: Color(0xFF006D77), // Pharma Teal
      colorScheme: ColorScheme.fromSeed(
        seedColor: Color(0xFF006D77),
      ),
      appBarTheme: AppBarTheme(
        backgroundColor: Color(0xFF006D77),
      ),
      // ... weitere Anpassungen
    );
  }
}


Warum Single-Tenant für easySale?

✅ Zwingende Gründe

1. Regulatorische Anforderungen - DSGVO: Daten müssen trennbar sein (Recht auf Datenexport/Löschung pro Kunde) - Branchen-spezifisch: Pharma hat andere Compliance als Baumarkt - Audit-Trail: Pro Kunde eigene Audit-Logs - Data Residency: Kunde kann fordern "Daten nur in Deutschland"

2. Sicherheit

// ❌ Multi-Tenant Risiko:
query.where('tenantId', isEqualTo: currentUser.tenantId);
// ☠️ Ein vergessener Filter = Komplettes Datenleck!

// ✅ Single-Tenant:
// Firestore Rules pro Projekt
// Kunde A kann PHYSISCH nicht auf Firebase B zugreifen
// Kein tenantId Filter nötig → kein Fehlerrisiko

3. Performance & Skalierung - Kunde A macht Black Friday Sale → beeinflusst Kunde B NICHT - Jeder Kunde kann unabhängig skalieren - Keine "Noisy Neighbor" Probleme

4. Customization-Freiheit - Kunde will eigene Firebase Functions? ✅ Kein Problem - Kunde will eigene Firestore Security Rules? ✅ Möglich - Kunde will eigene Domain/Subdomain? ✅ Einfach - Kunde will eigenes Backup-Schedule? ✅ Machbar

5. Verkauf & Vertragsgestaltung - Kunde kann eigenes Firebase-Konto verwenden (White-Label) - Klare Kostenzuordnung pro Kunde - Einfacher zu verkaufen: "Ihre eigene, dedizierte Instanz" - Premium-Pricing möglich durch Isolation

6. Disaster Recovery - Kunde A löscht versehentlich Daten? → Kunde B nicht betroffen - Rollback pro Kunde möglich - Backup-Strategie individuell anpassbar - Versionierung pro Kunde (Beta-Tester möglich)

⚠️ Nachteile ehrlich betrachtet

1. Höhere Infrastruktur-Kosten

Multi-Tenant:  1 × Firebase Blaze = €50/Monat für ALLE Kunden
Single-Tenant: 10 × Firebase Blaze = €50 × 10 = €500/Monat

Mitigation: An Kunden weitergeben (€50-100/Monat im SaaS-Preis)

2. Deployment-Aufwand

Multi-Tenant:  1 Deployment → alle Kunden aktualisiert
Single-Tenant: N Deployments → alle Kunden aktualisiert

Mitigation: CI/CD automatisiert (GitHub Actions Matrix)
Vorteil: Staggered Rollouts möglich (erst Testkunden, dann alle)

3. Monitoring-Komplexität

Multi-Tenant:  1 Dashboard für alles
Single-Tenant: 10 Dashboards zu überwachen

Mitigation: Firebase Admin SDK für aggregierte Dashboards
Vorteil: Klarere Fehler-Attribution ("Bug bei Kunde X")

💰 Kostenrechnung (reales Beispiel)

Annahme: 10 Kunden, mittlere Nutzung

Option A: Multi-Tenant

Firebase Blaze (shared):         €100/Monat
Zusätzliche Entwicklung:         €2000 (einmalig für tenant_id Logik)
Ongoing Security Audits:         €500/Monat (Risiko-Mitigation)
────────────────────────────────────────────
Monatlich: €600
Einmalig: €2000

Option B: Single-Tenant (unser Konzept)

Firebase Blaze × 10:             €500/Monat
CI/CD Automatisierung:           €1000 (einmalig)
Monitoring Dashboard:            €50/Monat
────────────────────────────────────────────
Monatlich: €550
Einmalig: €1000

Pro Kunde weiterberechnen:

SaaS Preis: €149/Monat
- Infrastruktur (€55): €94 Marge
- Support & Wartung: Inklusive
- Individuelle Anpassungen: Extra abrechenbar

Break-Even: Bei €149/Monat SaaS-Preis sind €55 Infrastruktur (37%) absolut vertretbar.

🎯 Fazit: Single-Tenant ist richtig für easySale

Gründe: 1. ✅ B2B-Kunden erwarten Isolation - "Shared Database" ist Verkaufshindernis 2. ✅ Compliance unumgänglich - DSGVO, Branchen-Spezifika 3. ✅ Individualisierung Verkaufsargument - "Maßgeschneidert für Sie" 4. ✅ Kosten weitergebbar - Im SaaS-Preis enthalten 5. ✅ Sicherheit > Kosten - Ein Datenleck zerstört Reputation 6. ✅ Wettbewerbsvorteil - Viele Konkurrenten haben Multi-Tenant (Risiko)

Was wir NICHT wollen:

❌ "Ups, wegen eines Bugs konnte Kunde A die Daten von Kunde B sehen"
❌ "Kunde X überlastet das System, alle sind langsam"
❌ "DSGVO-Auskunft dauert Wochen weil alles gemischt ist"

Was wir wollen:

✅ "Jeder Kunde hat garantiert isolierte Daten"
✅ "Skalierung pro Kunde individuell"
✅ "DSGVO-Auskunft per Knopfdruck (Firebase Export)"
✅ "Premium-Positionierung durch dedizierte Instanz"


Workflows & Szenarien

Szenario 1: Bugfix im Core

# 1. Bugfix im Core-Repo
cd easysale-core/core/apps/erp_system
# ... Code-Änderung ...

# 2. Testen
melos test:all  # Alle Packages testen

# 3. Commit & Push
git add .
git commit -m "fix: Artikel-Validierung korrigiert"
git push

# 4. GitHub Actions (notify-clients.yml) triggert automatisch
#    alle Client-Repos per repository_dispatch → auto-deploy.yml ✓
# Kein manuelles Cherry-Picking nötig!

Resultat: Alle Standard-Clients werden automatisch re-deployed.


Szenario 2: Client-spezifisches Feature

# Nur für Pharma AG: Chargennummer-Validierung

# 1. Im Client-Repo implementieren
cd easysale-client-pharma/erp/lib/blocs
# Erstelle pharma_articles_bloc.dart

cd easysale-client-pharma/erp/lib/pages
# Erstelle pharma_article_editor.dart

# 2. Nur diesen Client testen
cd easysale-client-pharma/erp
flutter test

# 3. Push auf Client-Repo → auto-deploy.yml baut und deployt
git push

Resultat: Nur Pharma AG hat das Feature, andere Clients unberührt.


Szenario 3: Neuer Kunde aufsetzen

# 1. Automatisches Setup (im Core-Repo)
./onboarding/create_client.sh

# Output:
# ✅ GitHub Repo easysale-client-neue_firma erstellt
# ✅ Flutter-App mit Git-Dependency auf Core angelegt
# ✅ Firebase-Projekte erstellt (dev + prod)
# ✅ .code-workspace mit Multi-Root + Core read-only
# ✅ GitHub Actions auto-deploy.yml konfiguriert
# ✅ client-registry.json aktualisiert

# 2. Client-Repo auschecken
cd ~/Development
git clone git@github.com:Tech-Schuppen/easysale-client-neue_firma.git

# 3. .code-workspace öffnen (Multi-Root mit Core read-only)
code easysale-client-neue_firma/.code-workspace

# 4. Client anpassen (optional)
cd easysale-client-neue_firma/erp/lib/config
# client_config.dart bearbeiten (Logo, Farben, etc.)

# 5. Push → auto-deploy.yml baut und deployt
git push

Zeitaufwand: < 1 Stunde für Standard-Client


Szenario 4: Kunde will krasse Änderungen

Beispiel: Automotive AG will komplett andere Artikel-Struktur

Entscheidung:

IF Änderung passt in Plugin-System:
  → Extension verwenden ✅

ELSE IF Änderung zu groß:
  → Eigener Fork für diesen Kunden
  → clients/automotive_custom/
  → Bugfixes manuell cherry-picken (nur für diesen)

Vorgehen:

# Option A: Plugin (bevorzugt)
cd easysale-client-automotive/erp
# Erstelle lib/plugins/automotive_plugin.dart
# Registriere in main.dart

# Option B: Fork (wenn nötig)
# Client pinnt auf eigenen Core-Branch:
# In pubspec.yaml des Clients:
dependencies:
  erp_system:
    git:
      url: git@github.com:Tech-Schuppen/easysale-core.git
      path: core/apps/erp_system
      ref: client-automotive-custom  # Eigener Branch


Implementierungsplan

Phase 1: Core-Vorbereitung (2-3 Tage)

1.1 Models erweitern - [ ] customData zu Article hinzufügen - [ ] customData zu Order hinzufügen - [ ] customData zu Customer hinzufügen - [ ] Migration für bestehende Daten

1.2 UI Override System - [ ] UiOverrideService erstellen - [ ] Article Detail Page anpassbar machen - [ ] Article List Item anpassbar machen - [ ] Order Detail Page anpassbar machen

1.3 BLoC Factory - [ ] BlocFactory Interface erstellen - [ ] DefaultBlocFactory implementieren - [ ] BLoC Provider umstellen

1.4 Service Interfaces - [ ] ArticleService Interface - [ ] OrderService Interface - [ ] CustomerService Interface - [ ] Default Implementations

1.5 Client Config System - [ ] ClientConfig Abstract Class - [ ] DefaultClientConfig implementieren - [ ] Config laden in App


Phase 2: Automatisierung (1-2 Tage)

2.1 Scripts (im Core-Repo)

onboarding/
├── create_client.sh       # Client-Repo + Firebase Setup
├── scripts/               # Einzelne Deploy-Skripte
└── templates/             # Workflow-Templates für Client-Repos

2.2 Melos konfigurieren (Core-Repo)

# melos.yaml (im Core-Repo für koordinierte Builds/Tests)
packages:
  - core/apps/**
  - core/shared

scripts:
  test:all:
    run: flutter test
  analyze:
    run: flutter analyze

2.3 CI/CD Setup - [x] notify-clients.yml im Core-Repo (triggert Client-Repos) - [x] auto-deploy.yml Template für Client-Repos - [x] client-registry.json für Versionszuordnung - [ ] CLIENT_REPOS_PAT Secret im Core-Repo anlegen


Phase 3: Beispiel-Client (1 Tag)

3.1 Demo-Client "Pharma AG" erstellen - [ ] Client Struktur anlegen - [ ] Model Extension (Chargennummer, etc.) - [ ] BLoC Extension (Validierung) - [ ] UI Anpassung (Extra Tab) - [ ] Service Extension (Spezielle Speicherlogik)

3.2 Dokumentation - [ ] README für Clients - [ ] Beispiel-Code dokumentieren - [ ] Video-Tutorial aufnehmen


Phase 4: Migration (Nach Bedarf)

Bestehende Kunden migrieren:

# Für jeden bestehenden Kunden:
1. Client-Ordner anlegen
2. Spezifische Anpassungen identifizieren
3. In Extensions umwandeln
4. Testen
5. Deployen


Vor- & Nachteile

✅ Vorteile

Aspekt Vorteil Impact
Wartbarkeit Bugfixes zentral → alle profitieren 🔥 Hoch
Code Reuse 80% gemeinsamer Code 🔥 Hoch
Skalierbarkeit 100+ Clients möglich 🔥 Hoch
Schnelligkeit Neuer Client in < 1h ⭐ Mittel
Testbarkeit Core Tests → alle Clients getestet 🔥 Hoch
Individualisierung Trotzdem kundenspezifisch anpassbar ⭐ Mittel
Klare Struktur Core vs. Client klar getrennt ⭐ Mittel

⚠️ Nachteile

Aspekt Nachteil Mitigation
Initiale Komplexität Core muss Extension-Points haben Einmalig, danach einfach
Overhead Mehr Abstraktion als einfacher Branch Lohnt sich ab ~5 Clients
Breaking Changes Core-Änderungen betreffen alle Semantic Versioning + Tests
Learning Curve Team muss Architektur verstehen Dokumentation + Training

🔀 Vergleich: Branch vs. Core+Extension vs. Multi-Tenant

Kriterium Branch-Ansatz Core+Extension (Single-Tenant) Multi-Tenant
Setup-Zeit ✅ Schnell (5 Min) ⚠️ Einmalig aufwändiger ⚠️ Komplex
Bugfixes ❌ Manuell in jeden Branch ✅ Automatisch für alle ✅ Einmal für alle
Datenisolation ✅ Total (separate Projekte) ✅ Total (separate Projekte) ❌ Logisch (tenant_id)
Sicherheit ✅ Sehr hoch ✅ Sehr hoch ⚠️ Risiko bei Bugs
Performance ✅ Isoliert ✅ Isoliert ❌ Shared Resources
DSGVO/Compliance ✅ Einfach ✅ Einfach ⚠️ Komplex
Individualisierung ✅ Total frei ⭐ Über Extensions ❌ Sehr limitiert
Wartbarkeit (5 Clients) ⚠️ Noch ok ✅ Sehr gut ✅ Sehr gut
Wartbarkeit (50 Clients) ❌ Unmöglich ✅ Gut ✅ Gut
Code Divergenz ❌ Nach 6 Mon. komplett anders ✅ Core bleibt gleich ✅ Kein Problem
Testing-Aufwand ❌ Jeder Branch einzeln ✅ Core + Stichproben ✅ Einmal
Kosten (Infrastruktur) 💰💰💰 Hoch (N×Firebase) 💰💰💰 Hoch (N×Firebase) 💰 Niedrig (1×Firebase)
Ausfallsicherheit ✅ Total isoliert ✅ Total isoliert ❌ Ein Ausfall = alle betroffen

Unser Ansatz = Beste Balance: - Single-Tenant Sicherheit & Isolation ✅ - Shared Codebase Wartbarkeit ✅
- Höhere Infrastruktur-Kosten in Kauf nehmen für Sicherheit & Compliance

📊 Entscheidungsmatrix

Wann Branch-Ansatz: - ✅ Nur 1-3 Kunden - ✅ Komplett unterschiedliche Apps - ✅ Keine gemeinsame Codebasis gewünscht

Wann Core+Extension: - ✅ 5+ Kunden (oder geplant) - ✅ 80% gemeinsame Funktionen - ✅ Regelmäßige Bugfixes - ✅ Langfristige Wartbarkeit wichtig

Unsere Empfehlung: Core+Extension mit Hybrid-Ansatz - Standard-Kunden → Core+Extension - Special-Cases → Fork erlaubt


Migration & Rollout

Rollout-Strategie

Empfohlenes Vorgehen:

Phase 1: Vorbereitung (1 Woche)
├── Core Extension Points einbauen ✓
├── Scripts & Tools vorbereiten ✓
└── Demo-Client als Proof of Concept ✓

Phase 2: Pilot (1-2 Wochen)
├── 1 bestehenden Kunden migrieren
├── Learnings sammeln
└── Dokumentation verbessern

Phase 3: Rollout (4-8 Wochen)
├── Weitere Kunden migrieren
├── Parallel: Neue Kunden im neuen System
└── Alte Branches deprecaten

Phase 4: Vollständig (3 Monate)
├── Alle Kunden migriert
├── Alte Branches archivieren
└── Nur noch Core+Extension

Migration-Checklist (pro Kunde)

Kunde: _________________

□ Firebase-Projekt analysiert
□ Kundenspezifische Anpassungen identifiziert
  □ Models erweitert?
  □ UI angepasst?
  □ Business Logic geändert?
  □ Eigene Features?

□ Client-Ordner angelegt
□ Firebase Config migriert
□ Anpassungen → Extensions umgewandelt
  □ Model Extensions (/lib/extensions/)
  □ UI Overrides (/lib/pages/)
  □ BLoC Extensions (/lib/blocs/)
  □ Service Extensions (/lib/services/)

□ Testing
  □ Unit Tests
  □ Integration Tests
  □ UAT mit Kunde

□ Deployment
  □ Staging deployed
  □ Kunde getestet
  □ Production deployed

□ Alter Branch archiviert

Best Practices

DO ✅

  1. Core schlank halten - Nur gemeinsame Funktionen
  2. Extension Points großzügig einbauen - Mehr ist besser
  3. Feature Flags nutzen - Für optionale Features
  4. Semantic Versioning - Breaking Changes klar kommunizieren
  5. Testing - Core Tests = alle Clients getestet
  6. Dokumentation - Jede Extension gut dokumentieren
  7. Code Reviews - Besonders bei Core-Änderungen

DON'T ❌

  1. Client-Code in Core - Niemals kundenbezogene Logik im Core
  2. Breaking Changes ohne Plan - Migration-Strategie erforderlich
  3. Zu viele Abstraktionen - Balance zwischen Flexibilität und Einfachheit
  4. Ungenutztes im Core - Entfernen wenn kein Client es braucht
  5. Direkte Firebase-Calls in UI - Immer über Services/BLoCs
  6. Hardcoded Werte - Immer über Config
  7. Unsichere customData - Validierung wichtig!

Nächste Schritte

Sofort

  1. Team-Meeting - Konzept mit allen durchgehen
  2. Entscheidung - Go/No-Go für Migration
  3. Priorisierung - Welche Clients zuerst?

Diese Woche

  1. Proof of Concept - Demo-Client aufsetzen
  2. Core erweitern - customData hinzufügen
  3. Scripts schreiben - create_client.sh

Nächste 2 Wochen

  1. Pilot-Migration - Einen Kunden migrieren
  2. Dokumentation - Developer Guide schreiben
  3. CI/CD Setup - Automatisches Deployment

Fragen & Antworten

F: Ist das Single-Tenant oder Multi-Tenant?

A: Definitiv Single-Tenant! Jeder Kunde bekommt: - ✅ Eigene Firebase-Instanz (eigene Datenbank) - ✅ Eigene App-URL / Domain - ✅ Eigene User-Accounts (getrennte Auth) - ✅ Eigene Deployment-Pipeline (eigenes Git-Repo + GitHub Actions) - ✅ Totale Datenisolation

Wir teilen nur den Quellcode (per Git-Dependency), nicht die Laufzeit-Umgebung.

F: Wenn es Single-Tenant ist, warum nicht einfach Branches?

A: Single-Tenant ≠ Separate Codebases!

Unser Ansatz:

Core-Repo + separate Client-Repos (Git-Dependencies) → Viele separate Deployments

Branch-Ansatz würde bedeuten:

Viele Code-Branches → Viele separate Deployments (Merge-Hölle)

Der Unterschied: Wir sparen uns die Mehrfach-Wartung von fast identischem Code!

F: Was wenn ein Kunde wirklich ALLES anders will?

A: Dann bekommt er einen Fork. Das Hybrid-Modell erlaubt beides. Für 80% der Kunden reicht Core+Extension, für die 20% Special-Cases gibt's Forks.

F: Wie gehen wir mit Breaking Changes im Core um?

A: 1. Semantic Versioning verwenden (Core-Branches: main, v1, v2) 2. Migrations-Guide schreiben 3. Deprecation Warnings 4. Clients können alte Version pinnen per ref: in pubspec.yaml

F: Kann ein Client mehrere Extensions kombinieren?

A: Ja! Ein Kunde kann gleichzeitig: - Model Extensions nutzen (Pharma-Felder) - UI überschreiben (eigenes Design) - BLoCs erweitern (eigene Validierung) - Services überschreiben (eigene Logik)

F: Was kostet die Migration?

A: - Core vorbereiten: 2-3 Tage - Scripts/Tools: 1-2 Tage - Pro Client migrieren: 2-4 Stunden - Bei 10 Kunden: ~1-2 Wochen Gesamt

F: Müssen alle Clients zur gleichen Zeit migriert werden?

A: Nein! Schrittweise möglich: - Neue Kunden → sofort im neuen System - Bestehende Kunden → nach und nach - Alte Branches → solange parallel bis alle migriert


Kontakt & Feedback

Dokumentations-Owner: Stefan Hafner
Datum: 24. Februar 2026
Review-Datum: _____

Feedback bitte an: [Team-Email/Slack-Channel]


Anhang

A: Beispiel-Code

Vollständige Beispiele sind in: - clients/pharma_ag_demo/ - Demo-Implementation - docs/examples/ - Code-Snippets - docs/tutorials/ - Video-Tutorials

B: Technologie-Stack

  • Monorepo: Melos (nur im Core-Repo)
  • Multi-Repo: Separate Git-Repos per Client (Git-Dependencies)
  • Frontend: Flutter (ERP + Shop)
  • Backend: Firebase (Firestore, Functions, Auth)
  • State Management: flutter_bloc
  • DI: Provider / GetIt
  • CI/CD: GitHub Actions (notify-clients + auto-deploy Pattern)
  • Deployment: Firebase Hosting

C: Glossar

Begriff Bedeutung
Single-Tenant Jeder Kunde hat eigene, isolierte Instanz
Multi-Instance Viele separate Instanzen, ein Codebase
Multi-Repo Separate Git-Repos für Core und Clients
Multi-Tenant ❌ NICHT unser Konzept (viele Kunden, eine Instanz)
Core Gemeinsame Basis-Funktionalität
Extension Kundenspezifische Erweiterung
Overlay Client-Layer über Core
Fork Separater Branch für Special-Client
customData Flexibles Daten-Feld für Extensions
Plugin Komplettes Feature-Modul
Shared Codebase Gemeinsamer Code, separate Deployments

D: Single-Tenant vs. Multi-Tenant Klarstellung

❌ Was wir NICHT haben (Multi-Tenant):

Eine zentrale App/Datenbank
└── tenant_id: "kunde_a"
└── tenant_id: "kunde_b"  
└── tenant_id: "kunde_c"
Problem: Sicherheitsrisiko, Performance-Sharing, Compliance-Probleme

✅ Was wir haben (Single-Tenant):

Kunde A: Firebase A → App A → Daten A (isoliert)
Kunde B: Firebase B → App B → Daten B (isoliert)
Kunde C: Firebase C → App C → Daten C (isoliert)
Vorteil: Totale Isolation, Sicherheit, Skalierung, DSGVO-konform

💡 Der Trick: Wir teilen den Quellcode (Development & Maintenance), aber deployen separate Instanzen (Runtime).


Ende der Dokumentation