6.7 KiB
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.
# 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/sharedexports 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 authprotectedProcedure— Session requiredmemberProcedure— Session + valid org membership (injectsorgIdandrole)
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 connectionBETTER_AUTH_SECRET/BETTER_AUTH_URL— Auth configSMTP_*— Email for magic linksNEXT_PUBLIC_APP_URL— Admin public URLEXPO_PUBLIC_API_URL— Mobile points to admin APIUPLOAD_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
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:
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:
sparten String[] @default([])
Migration steps
- Provision a PostgreSQL instance (Supabase, Neon, or self-hosted via Docker).
- Set
DATABASE_URLto apostgresql://connection string. - Update
schema.prisma: changeprovider, add@db.JsonBandString[]types. - Run
pnpm db:generateto regenerate the Prisma client. - Create a fresh migration:
pnpm db:migrate(this createspackages/shared/prisma/migrations/…). - All code that currently parses
landingPageFeatures/landingPageFooterasJSON.parse(string)must switch to reading them directly as objects (Prisma returns them asunknown/JsonValue).
Docker Compose (local PostgreSQL)
Add a postgres service to docker-compose.yml:
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-fnsfor formatting - Icons:
lucide-react(admin),@expo/vector-icons(mobile) - Schema changes: Always run
pnpm db:generateafter editingpackages/shared/prisma/schema.prisma - tRPC client (mobile): configured in
apps/mobile/lib/trpc.ts, usessuperjsontransformer - Enum fields: Stored as
Stringin SQLite (enforced via Zod); after PostgreSQL migration, consider converting to nativeenumtypes