# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview **InnungsApp** is a multi-tenant SaaS platform for German trade guilds (Innungen). It consists of: - **Admin Dashboard**: Next.js 15 web app for guild administrators - **Mobile App**: Expo React Native app for guild members (iOS + Android) - **Shared Package**: Prisma ORM schema, types, and utilities ## Commands All commands run from `innungsapp/` root unless noted. ```bash # Development pnpm install # Install all workspace dependencies pnpm dev # Start all apps in parallel (Turborepo) # Per-app dev pnpm --filter admin dev # Admin only (Next.js on :3000) pnpm --filter mobile dev # Mobile only (Expo) cd apps/mobile && npx expo run:android cd apps/mobile && npx expo run:ios # Type checking & linting pnpm type-check # tsc --noEmit across all apps pnpm lint # ESLint across all apps # Database (Prisma via shared package) pnpm db:generate # Regenerate Prisma client after schema changes pnpm db:migrate # Run migrations (dev) pnpm db:push # Push schema without migration (prototype) pnpm db:studio # Open Prisma Studio pnpm db:seed # Seed with test data pnpm db:reset # Drop + re-migrate + re-seed # Deployment vercel --cwd apps/admin # Deploy admin to Vercel cd apps/mobile && eas build --platform all --profile production cd apps/mobile && eas submit --platform all ``` ## Architecture ### Monorepo Structure - **pnpm Workspaces + Turborepo** — `apps/admin`, `apps/mobile`, `packages/shared` - `packages/shared` exports Prisma client, schema types, and shared utilities - Both apps import from `@innungsapp/shared` ### Data Flow ``` Mobile App (Expo) │ ▼ HTTP (tRPC) Admin App (Next.js API Routes) │ ▼ Prisma ORM PostgreSQL Database ``` The mobile app calls the admin app's tRPC API (`/api/trpc`). There is no separate backend — the Next.js app serves both the admin UI and the API. ### tRPC Procedure Hierarchy Three protection levels in `apps/admin/server/trpc.ts`: - `publicProcedure` — No auth - `protectedProcedure` — Session required - `memberProcedure` — Session + valid org membership (injects `orgId` and `role`) Routers are in `apps/admin/server/routers/`: `members`, `news`, `termine`, `stellen`, `organizations`. ### Multi-Tenancy Every resource (member, news, event, job listing) is scoped to an `Organization`. The `memberProcedure` extracts `orgId` from the session and all queries filter by it. Org plan types: `pilot`, `standard`, `pro`, `verband`. ### Authentication - **better-auth** with magic links (email-based, passwordless) - Admin creates a member → email invitation sent via SMTP → member sets up account - Session stored in DB; mobile app persists session token in AsyncStorage - Auth handler: `apps/admin/app/api/auth/[...all]/route.ts` ### Mobile Routing (Expo Router) File-based routing with two route groups: - `(auth)/` — Login, check-email (unauthenticated) - `(app)/` — Tab navigation: home, members, news, stellen, termine, profil (requires session) Zustand (`store/auth.store.ts`) holds auth state; React Query handles server state via tRPC. ### Admin Routing (Next.js App Router) - `/login` — Magic link login - `/dashboard` — Protected layout with sidebar - `/dashboard/mitglieder` — Member CRUD - `/dashboard/news` — News management - `/dashboard/termine` — Event management - `/dashboard/stellen` — Job listings - `/dashboard/einstellungen` — Org settings (AVV acceptance) File uploads are stored locally in `apps/admin/uploads/` and served via `/api/uploads/[...path]`. ### Environment Variables Required in `apps/admin/.env` (see `.env.example`): - `DATABASE_URL` — PostgreSQL connection - `BETTER_AUTH_SECRET` / `BETTER_AUTH_URL` — Auth config - `SMTP_*` — Email for magic links - `NEXT_PUBLIC_APP_URL` — Admin public URL - `EXPO_PUBLIC_API_URL` — Mobile points to admin API - `UPLOAD_DIR` / `UPLOAD_MAX_SIZE_MB` — File storage ## Planned: SQLite → PostgreSQL Migration The current schema uses **SQLite** (`packages/shared/prisma/schema.prisma`). The migration target is **PostgreSQL** (production-grade, enables JSONB and native arrays). ### What changes in `schema.prisma` ```prisma datasource db { provider = "postgresql" // was: "sqlite" url = env("DATABASE_URL") } ``` ### Fields to convert to `@db.JsonB` These fields are currently stored as JSON-encoded `String?` in SQLite and must become proper JSONB columns in PostgreSQL: | Model | Field | Prisma annotation | |---|---|---| | `Organization` | `landingPageFeatures` | `@db.JsonB` | | `Organization` | `landingPageFooter` | `@db.JsonB` | Example after migration: ```prisma landingPageFeatures Json? @map("landing_page_features") @db.JsonB landingPageFooter Json? @map("landing_page_footer") @db.JsonB ``` ### Fields to convert to native PostgreSQL arrays `Organization.sparten` is stored as `String?` (comma-separated or JSON) in SQLite. In PostgreSQL it becomes: ```prisma sparten String[] @default([]) ``` ### Migration steps 1. Provision a PostgreSQL instance (Supabase, Neon, or self-hosted via Docker). 2. Set `DATABASE_URL` to a `postgresql://` connection string. 3. Update `schema.prisma`: change `provider`, add `@db.JsonB` and `String[]` types. 4. Run `pnpm db:generate` to regenerate the Prisma client. 5. Create a fresh migration: `pnpm db:migrate` (this creates `packages/shared/prisma/migrations/…`). 6. All code that currently parses `landingPageFeatures` / `landingPageFooter` as `JSON.parse(string)` must switch to reading them directly as objects (Prisma returns them as `unknown` / `JsonValue`). ### Docker Compose (local PostgreSQL) Add a `postgres` service to `docker-compose.yml`: ```yaml postgres: image: postgres:16-alpine environment: POSTGRES_DB: innungsapp POSTGRES_USER: innungsapp POSTGRES_PASSWORD: secret volumes: - pg_data:/var/lib/postgresql/data ports: - "5432:5432" volumes: pg_data: ``` Then set `DATABASE_URL=postgresql://innungsapp:secret@localhost:5432/innungsapp`. ## Key Conventions - **Styling**: Tailwind CSS in admin; NativeWind v4 (Tailwind syntax) in mobile - **Validation**: Zod schemas defined inline with tRPC procedures - **Dates**: `date-fns` for formatting - **Icons**: `lucide-react` (admin), `@expo/vector-icons` (mobile) - **Schema changes**: Always run `pnpm db:generate` after editing `packages/shared/prisma/schema.prisma` - **tRPC client (mobile)**: configured in `apps/mobile/lib/trpc.ts`, uses `superjson` transformer - **Enum fields**: Stored as `String` in SQLite (enforced via Zod); after PostgreSQL migration, consider converting to native `enum` types