Files
HausApp/CLAUDE.md
René Schober 9ddc7c6d7a Production deployment setup + feature complete
- Dockerfile + deploy.sh for Hetzner server
- Email verification via Better Auth + Resend
- Invite code flow (6-digit OTP, generate/join)
- Settlement share percent fix (payer vs debtor)
- OCR scanner fixes (date display, retry, viewfinder)
- app.json icon/splash/adaptive-icon configured
- iOS deployment target 15.5 (ML Kit requirement)
- DB migration 0014: household_invitations table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 11:54:22 +01:00

7.7 KiB

HaushaltsApp — Claude Instructions

Projektübersicht

Mobile-SaaS App für Haushaltsverwaltung: Haushaltsbuch, Urlaubsbudget, Echtzeit-Einkaufsliste.

Stack:

  • Monorepo: Bun Workspaces + Turborepo
  • Backend: Hono (Bun) — apps/server/
  • Mobile: React Native + Expo + expo-router — apps/native/
  • Web (Dashboard): React + TanStack Router + Vite — apps/web/
  • Database: PostgreSQL + Drizzle ORM — packages/db/
  • Auth: Better Auth — packages/auth/
  • Shared Types/Schemas: Zod — packages/shared/
  • UI (Web): shadcn/ui (base-lyra) — packages/ui/
  • UI (Native): HeroUI Native + Uniwind

Architektur-Entscheidungen

  • Multi-Tenant via householdId: Jeder Haushalt ist ein Tenant. Alle Daten (Transaktionen, Einkaufslisten etc.) sind an eine householdId gebunden. householdId wird via x-household-id HTTP-Header übergeben.
  • Subscription Plans: free, pro, family — Feature-Gates in plan.middleware.ts, Definitionen in packages/shared/src/constants/plans.ts.
  • Better Auth übernimmt User/Session-Management. Die packages/auth/ enthält die Server-seitige Konfiguration.
  • Shared Package (@haushaltsApp/shared): Zod-Schemas und TypeScript-Types werden im shared package definiert und sowohl vom Backend als auch vom Frontend verwendet.
  • Drizzle Schema: Alle App-Tabellen in packages/db/src/schema/app.ts, Auth-Tabellen in packages/db/src/schema/auth.ts.

Development starten

# Alle Services starten
bun run dev

# Einzelne Services
bun run dev:server   # API auf http://localhost:3000
bun run dev:web      # Web auf http://localhost:3001
bun run dev:native   # Expo (Metro Bundler)

# Datenbank
bun run db:start     # PostgreSQL via Docker starten
bun run db:generate  # Drizzle Migrationen generieren
bun run db:migrate   # Migrationen anwenden
bun run db:studio    # Drizzle Studio öffnen

Testing-Konventionen

Backend (apps/server/)

  • Framework: bun:test (built-in, kein extra Package)
  • Ort: apps/server/src/__tests__/routes/ und apps/server/src/__tests__/services/
  • Konvention: Route-Tests testen den HTTP-Layer direkt via app.request(). Services werden unit-getestet.
  • Ausführen: bun run test:api oder bun test apps/server/src/__tests__
// Beispiel Route-Test
import { describe, expect, it } from "bun:test";
import app from "../../index";

describe("GET /health", () => {
  it("returns 200 with status ok", async () => {
    const res = await app.request("/health");
    expect(res.status).toBe(200);
  });
});

Mobile (apps/native/)

  • Framework: bun:test — kein Jest/Babel/jest-expo (Jest + Bun's .bun/ Store sind inkompatibel mit RN's ESM setup files)
  • Was wird getestet: Stores, Hooks, Utils, API-Client — reine Business Logic, kein React Native Rendering
  • Was NICHT getestet wird: UI-Komponenten, Screens — Rendering wird manuell via Expo Go verifiziert
  • Ort: apps/native/src/__tests__/ spiegelt apps/native/src/ Struktur
  • Ausführen: bun run test:mobile oder bun test apps/native/src/__tests__
// Beispiel Store-Test
import { describe, expect, it, beforeEach } from "bun:test";
import { useAuthStore } from "../../stores/auth.store";

describe("authStore", () => {
  beforeEach(() => {
    useAuthStore.setState({ user: null, token: null, isAuthenticated: false });
  });

  it("setUser authenticates the user", () => {
    useAuthStore.getState().setUser({ id: "1", name: "Test", email: "t@t.com" });
    expect(useAuthStore.getState().isAuthenticated).toBe(true);
  });
});

Coding-Konventionen

Allgemein

  • TypeScript strict mode überall — kein any, kein type-casting ohne Kommentar
  • Zod-first: Alle API-Inputs werden mit Zod-Schemas aus @haushaltsApp/shared validiert
  • Named exports bevorzugen (default exports nur bei Expo/React Router Screens)

Naming

  • Files: kebab-case.ts / PascalCase.tsx für React-Komponenten
  • Variables/Functions: camelCase
  • Types/Interfaces: PascalCase
  • Database tables: snake_case (Drizzle convention)
  • Zod schemas: camelCaseSchema (z.B. createTransactionSchema)
  • Route files: feature.routes.ts
  • Middleware files: feature.middleware.ts
  • Service files: feature.service.ts

API-Design

  • Alle Endpoints unter /api/ prefix
  • Auth-Check via authMiddleware + requireAuth
  • Tenant-Check via tenantMiddleware + requireHousehold
  • Plan-Gates via requireFeature('featureName')
  • HTTP-Status-Codes: 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found

Ordnerstruktur (Backend)

apps/server/src/
├── routes/         # HTTP-Layer, minimale Logik
├── services/       # Business Logic, kein direkter DB-Code in routes
├── middleware/     # Auth, Tenant, Plan Feature Gates
├── websocket/      # WebSocket Handler
├── lib/            # DB-Instance, Utilities
└── __tests__/      # Tests spiegeln src/-Struktur

State Management (Mobile)

  • Zustand für globalen Client-State (User, aktueller Haushalt)
  • TanStack Query für Server-State (Daten aus der API)
  • Kein direktes Fetch in Komponenten — immer Custom Hooks oder TanStack Query

Known Constraints

React Native Testing

Jest + Bun's Content-Addressable Store (.bun/) sind strukturell inkompatibel für RN Rendering-Tests. jest-expo setzt setupFiles mit absoluten .bun/ Pfaden — kein Resolver oder transformIgnorePatterns kann das abfangen.

Entscheidung: bun:test für Business Logic (Stores, Hooks, Utils, Services). Rendering wird via Expo Go verifiziert. Rendering-Tests werden nachgereicht wenn Bun/jest-expo das nativ lösen.

Web App (apps/web)

TanStack Router generiert routeTree.gen.ts erst bei vite dev. check-types für web daher nur nach einmaligem dev-Run aussagekräftig.

Better Auth Mobile Setup

  • bearer Plugin ist Pflicht in packages/auth/src/index.ts
  • Mobile Clients nutzen Authorization: Bearer <token> Header
  • Ohne bearer Plugin: getSession() gibt null zurück für alle Mobile Requests — stiller Auth-Fehler, alle Requests landen als 401

Expo Router — Redirect Pattern (FINAL)

FALSCH: Session-Guards in mehreren Layouts gleichzeitig → Ping-Pong Loop zwischen (auth) und (app) Layout

RICHTIG:

  • index.tsx: statischer <Redirect href="/(auth)/login" /> — kein useSession, kein useEffect
  • (auth)/_layout.tsx: KEIN Guard, nur Stack-Definition
  • (app)/_layout.tsx: EINZIGER Guard
    • kein session → /(auth)/login
    • kein householdId → /(auth)/onboarding
    • sonst: render children

Ein Guard, eine Quelle der Wahrheit.

Household / Organization Bridge

  • households.id === Better Auth organization.id (gleiche UUID)
  • Reihenfolge beim Onboarding:
    1. organization.create()organizationId
    2. INSERT INTO households { id: organizationId, ... } (via /api/households/setup)
    3. seedDefaultCategories(organizationId)
  • Kein harter DB-FK von households zu organization — application-level check reicht, Better Auth Schema ist extern

Projektstand

Phase Status Details
Phase 1 — Foundation Monorepo, DB Schema, Shared Types, Stubs
Phase 2 — Auth Flow Better Auth, Organization Plugin, Apple, Auth Screens
Phase 3 — Transactions Full Stack 9 API Tests, 7 Mobile Tests, Tenant Isolation bestätigt
Phase 4 — Dashboard
Phase 5 — Urlaubsbudget
Phase 6 — OCR Scanner
Phase 7 — Einkaufsliste (WebSockets)

Skills

Projekt-spezifische Skills unter apps/api/.agents/skills/ (noch leer, wird befüllt).

@apps/api/.agents/skills/