Files
crewli/CLAUDE.md
bert.hausmans 0d24506c89 feat: consolidate frontend API layer, add query-client, and harden backend Fase 1
Frontend:
- Consolidate duplicate API layers into single src/lib/axios.ts per app
- Remove src/lib/api-client.ts and src/utils/api.ts (admin)
- Add src/lib/query-client.ts with TanStack Query config per app
- Update all imports and auto-import config

Backend:
- Fix organisations.billing_status default to 'trial'
- Fix user_invitations.invited_by_user_id to nullOnDelete
- Add MeResource with separated app_roles and pivot-based org roles
- Add cross-org check to EventPolicy view() and update()
- Restrict EventPolicy create/update to org_admin/event_manager (not org_member)
- Attach creator as org_admin on organisation store
- Add query scopes to Event and UserInvitation models
- Improve factories with Dutch test data
- Expand test suite from 29 to 41 tests (90 assertions)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:35:34 +02:00

5.0 KiB

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 <script setup lang="ts"> — never the Options API
  • Type props with defineProps<{...}>()
  • Declare emits with defineEmits<{...}>()

API calls

  • Use TanStack Query (useQuery / useMutation) for all API calls
  • Never call axios directly from a component — always via a composable under composables/
  • Use Pinia stores for cross-component state — no prop drilling

Naming

  • DB columns: snake_case
  • TypeScript / JS variables: camelCase
  • Vue components: PascalCase (e.g. ShiftAssignPanel.vue)
  • Composables: use prefix (e.g. useShifts.ts)
  • Pinia stores: use prefix + Store suffix (e.g. useEventStore.ts)

UI

  • Always use Vuexy/Vuetify for layout, forms, tables, dialogs
  • Do not write custom CSS when a Vuetify utility class exists
  • Responsive: mobile-first, usable from 375px width

Forbidden patterns

  • Never: $user->role === 'admin' (use policies)
  • Never: Model::all() without a where clause (always scope)
  • Never: leave dd() or var_dump() in code
  • Never: hard-code .env values in code
  • Never: use JSON columns for data you need to filter on
  • Never: UUID v4 as primary key (use HasUlids)

Order of work for each new module

  1. Create and run migration(s)
  2. Eloquent model with relationships, scopes, and HasUlids
  3. Factory for test data
  4. Policy for authorization
  5. Form request(s) for validation
  6. API resource for response shaping
  7. Resource controller
  8. Register routes in api.php
  9. Write and run PHPUnit feature tests
  10. Vue composable for API calls (e.g. useShifts.ts)
  11. Pinia store if cross-component state is needed
  12. Vue page component
  13. Add route in Vue Router