- 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>
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 einehouseholdIdgebunden.householdIdwird viax-household-idHTTP-Header übergeben. - Subscription Plans:
free,pro,family— Feature-Gates inplan.middleware.ts, Definitionen inpackages/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 inpackages/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/undapps/server/src/__tests__/services/ - Konvention: Route-Tests testen den HTTP-Layer direkt via
app.request(). Services werden unit-getestet. - Ausführen:
bun run test:apioderbun 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__/spiegeltapps/native/src/Struktur - Ausführen:
bun run test:mobileoderbun 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/sharedvalidiert - Named exports bevorzugen (default exports nur bei Expo/React Router Screens)
Naming
- Files:
kebab-case.ts/PascalCase.tsxfü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
bearerPlugin ist Pflicht inpackages/auth/src/index.ts- Mobile Clients nutzen
Authorization: Bearer <token>Header - Ohne
bearerPlugin:getSession()gibtnullzurü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
- kein session →
Ein Guard, eine Quelle der Wahrheit.
Household / Organization Bridge
households.id === Better Auth organization.id(gleiche UUID)- Reihenfolge beim Onboarding:
organization.create()→organizationIdINSERT INTO households { id: organizationId, ... }(via/api/households/setup)seedDefaultCategories(organizationId)
- Kein harter DB-FK von
householdszuorganization— 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/