3 Architektur
SurfaceScratcher edited this page 2026-05-15 08:39:20 +02:00

Architektur

Monorepo-Struktur

mrrmlabapp/
├── apps/
│   ├── api/          # NestJS REST API
│   ├── web/          # Vite React Web-App
│   ├── mobile/       # Expo React Native App
│   └── ocr/          # Python FastAPI OCR-Service
├── packages/
│   ├── shared-types/ # Zod-Schemas + TypeScript-Typen
│   ├── api-client/   # Typisierter HTTP-Client + TanStack-Query-Hooks
│   ├── auth/         # Cross-Platform OIDC-Client
│   ├── ui/           # Tamagui Design System
│   ├── feature-shopping-list/  # Einkaufslisten-Feature
│   ├── feature-lists/          # Listen-Container
│   └── config/       # TypeScript/ESLint/Prettier-Presets
├── infra/
│   ├── android-builder/  # Docker-Image für Android-CI-Builds
│   └── fdroid/           # F-Droid Server-Setup
├── deploy/               # Docker Compose (Produktion)
└── .drone.yml            # CI/CD-Pipelines

Tooling: pnpm Workspaces + Turborepo

Apps

apps/api — NestJS Backend

  • Port: 3000
  • Feature-modulare Architektur: Jedes Feature ist ein eigenes NestJS-Modul (controller / service / schema / dto)
  • Datenbank: PostgreSQL via Drizzle ORM, automatische Migrationen beim Start
  • Auth: Globaler JwtAuthGuard, Dev-Passthrough wenn KC_ISSUER nicht gesetzt
  • Module: HealthModule, AuthModule, ListsModule, StoresModule, MailModule

apps/web — Vite React Web-App

  • Port: 5173 (Dev), 80 (Produktion via nginx)
  • Routing: TanStack Router v1 (file-based, typsicher)
  • State: TanStack Query v5 für Server State
  • UI: Tamagui (identische Komponenten wie Mobile)

apps/mobile — Expo React Native

  • Package ID: de.mrrm.mrrmlab
  • Router: Expo Router (file-based)
  • Vertrieb: F-Droid (selbst gehostetes Repo)
  • Auth: OIDC mit System-Browser (expo-web-browser) + Deep Link

apps/ocr — Python FastAPI OCR-Service

  • Port: 8000
  • Funktion: Texterkennung aus Bildern (Einkaufszettel-Scan)
  • Modell: EasyOCR, fine-tunable via finetune/train.py
  • Endpoints: /ocr, /ocr/debug, /ocr/save-training, /health

Packages

@mrrmlab/shared-types

Single Source of Truth für alle API-Datenstrukturen. Zod-Schemas werden für Backend-Validierung UND Frontend-Typen verwendet. Verwendet react-native Export-Condition für Metro (TypeScript-Quellcode direkt statt Build-Artefakt).

@mrrmlab/api-client

Typisierter HTTP-Client. ApiClient aggregiert alle Resource-Clients (ListsResource, StoresResource, MailResource). Zod-Validierung aller API-Responses.

@mrrmlab/auth

Cross-Platform OIDC-Client. Implementiert Authorization Code Flow mit PKCE. Nutzt @noble/hashes für SHA-256 (kein crypto.subtle nötig → React Native kompatibel). Token-Speicherung via pluggbares TokenStore-Interface.

@mrrmlab/ui

Tamagui-basiertes Design System. Funktioniert auf Web und React Native ohne Code-Änderungen.

@mrrmlab/config

Geteilte TypeScript-, ESLint- und Prettier-Konfigurationen für alle Packages und Apps.

Datenfluss (Beispiel: Einkaufsliste laden)

Mobile/Web
  → AuthClient.getAccessToken()        (Token aus SecureStore/localStorage)
  → ApiClient.lists.getAll()           (HTTP GET /lists + Bearer Token)
  → NestJS JwtAuthGuard               (JWT-Verifikation via Keycloak JWKS)
  → ListsService.findAll(ownerSub)     (Drizzle Query, gefiltert nach User)
  → Response mit Zod validiert         (shared-types Schema)
  → TanStack Query Cache               (automatisches Re-fetch, Stale-While-Revalidate)

Frontend-Konventionen

Responsive Layouts: ein stabiler Tree, kein Parent-Swap

Regel: Layouts, die children umhüllen (z. B. Nav-Wrapper, App-Shell), dürfen nicht bei Viewport-Änderung den Parent-Komponententyp wechseln.

Anti-Pattern (vermeiden):

// Wechselt zwischen zwei unterschiedlichen Parents auf Resize
const isWide = useIsWideScreen();
return isWide ? <WideLayout>{children}</WideLayout> : <NarrowLayout>{children}</NarrowLayout>;

Beim Überschreiten des Breakpoints sieht React unterschiedliche Parent-Typen → der gesamte children-Subtree wird unmount-/remountet. Lokaler State von Routen geht verloren — z. B. eine laufende Mail-Kategorisierung bricht beim Drehen des Handys ab (siehe #300).

Pattern (so machen):

// Ein stabiler Tree, Sichtbarkeit per CSS-Media-Query
<YStack flex={1}>
  <NarrowHeader $gtSm={{ display: 'none' }} />
  <XStack flex={1}>
    <WideSidebar display="none" $gtSm={{ display: 'flex' }} />
    <YStack flex={1}>{children}</YStack>
  </XStack>
</YStack>

Beide Nav-Varianten sind immer gemountet, Tamagui-Media-Props ($gtSm) togglen nur display. {children} bleibt durch Viewport-Wechsel mounted.

Mobile-Pendant: react-native-drawer-layout (hinter Expo Routers <Drawer>) hält children ebenfalls am gleichen Tree-Slot — reaktive drawerType-Wechsel sind dort sicher.

Folge-Regel für langlebigen In-Flight-State: Asynchrone Operationen, die einen Komponenten-Unmount überleben sollen (Streaming-Imports, Long-Polling, Hintergrund-Klassifikation), gehören perspektivisch in einen App-weiten Store oder Context oberhalb des Outlets — nicht in useState einer Route-Komponente.