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>
5.0 KiB
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 backendapps/admin/— Super Admin SPAapps/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 organisationsapps/app/— Organizer: event management per organisationapps/portal/— External users: one app, two access modes:- Login-based (
auth:sanctum): volunteers, crew — persons withuser_id - Token-based (
portal.tokenmiddleware): artists, suppliers, press — persons withoutuser_id
- Login-based (
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
OrganisationScopeas 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
HasUlidstrait 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:
useprefix (e.g.useShifts.ts) - Pinia stores:
useprefix +Storesuffix (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 awhereclause (always scope) - Never: leave
dd()orvar_dump()in code - Never: hard-code
.envvalues 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
- Create and run migration(s)
- Eloquent model with relationships, scopes, and
HasUlids - Factory for test data
- Policy for authorization
- Form request(s) for validation
- API resource for response shaping
- Resource controller
- Register routes in
api.php - Write and run PHPUnit feature tests
- Vue composable for API calls (e.g.
useShifts.ts) - Pinia store if cross-component state is needed
- Vue page component
- Add route in Vue Router