# Crewli — Claude Code Instructions ## Project context Crewli is a multi-tenant SaaS platform for event and festival management. Built for a professional volunteer organisation, with potential to expand as SaaS. Design document: `/resources/design/design-document.md` ## Tech stack - Backend: PHP 8.2+, Laravel 12, Sanctum, Spatie Permission, MySQL 8, Redis - Frontend: TypeScript, Vue 3 (Composition API), Vuexy/Vuetify, Pinia, TanStack Query - Testing: PHPUnit (backend), Vitest (frontend) ## Repository layout - `api/` — Laravel backend - `apps/admin/` — Super Admin SPA - `apps/app/` — Organizer SPA (main product app) - `apps/portal/` — External portal (volunteers, artists, suppliers, etc.) ## Apps and portal architecture - `apps/admin/` — Super Admin: platform management, creating organisations - `apps/app/` — Organizer: event management per organisation - `apps/portal/` — External users: one app, two access modes: - Login-based (`auth:sanctum`): volunteers, crew — persons with `user_id` - Token-based (`portal.token` middleware): artists, suppliers, press — persons without `user_id` ### CORS Configure three frontend origins in both Laravel (`config/cors.php` via env) and the Vite dev server proxy: - admin: `localhost:5173` - app: `localhost:5174` - portal: `localhost:5175` **Production (`crewli.app`):** API `https://api.crewli.app`, SPAs `https://admin.crewli.app`, `https://app.crewli.app`, `https://portal.crewli.app` — see `api/.env.example` for `FRONTEND_*` and `SANCTUM_STATEFUL_DOMAINS`. **`crewli.nl`** is only for a future marketing site; this application stack uses **`crewli.app`** (not `.nl` for API, SPAs, or transactional mail). ## Backend rules (strict) ### Multi-tenancy - Every query on event data **must** scope on `organisation_id` - Use `OrganisationScope` as an Eloquent global scope on all event-related models - Never use raw ID checks in controllers — always use policies ### Controllers - Use resource controllers (`index` / `show` / `store` / `update` / `destroy`) - Namespace: `App\Http\Controllers\Api\V1\` - Return all responses through API resources (never return raw model attributes) - Validate with form requests (never inline `validate()`) ### Models - Use the `HasUlids` trait on all business models (not UUID v4) - Soft deletes on: Organisation, Event, FestivalSection, Shift, ShiftAssignment, Person, Artist - No soft deletes on: CheckIn, BriefingSend, MessageReply, ShiftWaitlist (audit records) - JSON columns **only** for opaque configuration — never for queryable/filterable data ### Database - Primary keys: ULID via `HasUlids` (not UUID v4, not auto-increment on business tables) - Create migrations in dependency order: foundation first, then dependent tables - Always add composite indexes as documented in the design document (section 3.5) ### Roles and permissions - Use Spatie `laravel-permission` - Check roles via `$user->hasRole()` and policies — never hard-code role strings in controllers - Three levels: app (`super_admin`), organisation (`org_admin` / `org_member`), event (`event_manager`, etc.) ### Testing - Write PHPUnit feature tests per controller - Minimum per endpoint: happy path + unauthenticated (401) + wrong organisation (403) - Use factories for all test data - After each module: `php artisan test --filter=ModuleName` ## Frontend rules (strict) ### Vue components - Always `