186 lines
6.7 KiB
Markdown
186 lines
6.7 KiB
Markdown
# 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
|