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 wennKC_ISSUERnicht 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.