The admin SPA (apps/admin/) has been retired. Its functionality now lives in apps/app/ under /platform/* routes for super_admin users. Updated all documentation to reflect: 2 SPAs instead of 3, removed FRONTEND_ADMIN_URL/port 5173 references, changed production URL from app.crewli.app to crewli.app. Retired admin-specific security audit findings (A13-2, A13-4, A13-5, A13-7) and APPS-01 backlog item. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 KiB
Crewli — Claude Code Master Prompt
Plak dit BOVEN elke task. Vervang [TASK] onderaan.
MANDATORY PREAMBLE — READ BEFORE DOING ANYTHING
Read /CLAUDE.md and /dev-docs/SCHEMA.md in full before starting. These are
your single source of truth. Do not deviate from them. Do not make assumptions
about the database schema — always verify against SCHEMA.md. Also read
/dev-docs/API.md for the existing API contract.
If the task involves a module that already has existing code, read that code first. Understand the current state before making changes.
PROJECT CONTEXT
Crewli is a multi-tenant SaaS platform for event and festival management.
- Backend: Laravel 12 REST API (no Blade views, no Inertia), Sanctum auth, Spatie Permission, MySQL 8, Redis
- Frontend: Two standalone Vue 3 + TypeScript SPAs on Vuexy 9.5 /
Vuetify 3.10 —
apps/app/(port 5174),apps/portal/(port 5175) - State: Pinia + TanStack Vue Query
- Forms: VeeValidate + Zod
- API base path:
/api/v1/ - Testing: PHPUnit (backend), Vitest (frontend)
Repository structure
crewli/
├── api/ # Laravel 12 backend
│ ├── app/
│ │ ├── Http/
│ │ │ ├── Controllers/Api/V1/
│ │ │ ├── Middleware/
│ │ │ └── Requests/ # Form Requests per endpoint
│ │ ├── Models/
│ │ ├── Policies/
│ │ ├── Services/ # Business logic (NOT in controllers)
│ │ ├── Enums/ # PHP Enums for all status/type fields
│ │ ├── Events/ + Listeners/
│ │ └── Jobs/ # Queued jobs (briefings, notifications)
│ ├── database/
│ │ ├── migrations/
│ │ ├── factories/
│ │ └── seeders/
│ └── tests/Feature/Api/V1/
├── apps/
│ ├── app/ # Organizer + Platform Admin SPA (main app)
│ │ └── src/
│ │ ├── lib/axios.ts # THE ONLY axios instance
│ │ ├── composables/api/ # TanStack Query composables
│ │ ├── stores/ # Pinia stores
│ │ ├── types/ # TypeScript interfaces
│ │ └── pages/
│ └── portal/ # Dual-mode portal
├── dev-docs/ # Developer documentation (source of truth)
│ ├── SCHEMA.md
│ ├── API.md
│ ├── BACKLOG.md
│ ├── design-document.md
│ ├── dev-guide.md
│ └── start-guide.md
├── docs/ # VitePress end-user documentation (Dutch)
├── CLAUDE.md # This file's companion — workspace rules
└── .cursorrules
Key architectural decisions (final, non-negotiable)
- ULID primary keys on all business tables via
HasUlids— NEVER UUID v4 - Integer auto-increment on pure pivot tables only
- JSON columns exclusively for opaque config (settings, blocks) — never for queryable data
- Multi-tenancy via
OrganisationScopeEloquent Global Scope on all event-related models - Soft delete per table type as documented in SCHEMA.md — immutable audit records (check_ins, form_submissions, briefing_sends) do NOT get soft delete
- Portal dual-mode: Sanctum session for volunteers/crew (persistent
identity),
portal.tokenmiddleware for artists/suppliers/press (event-specific, no account)
ZERO-COMPROMISE RULES — VIOLATIONS ARE BLOCKERS
These rules are absolute. No workarounds. No "we'll fix it later". No partial implementations. If something cannot be done properly, STOP and report it.
Architecture & design
-
ARCHITECTURE FIRST. If the task requires a new pattern, data structure, or integration that isn't documented in CLAUDE.md or SCHEMA.md — stop and ask. Do not invent architecture. Write an Architecture Decision Record (ADR) in
/dev-docs/decisions/if a significant design choice is needed. -
DELETE OVER ADAPT. If you find duplicate logic, conflicting patterns, or legacy code that does the same thing differently: delete the worse version. Do not build alongside it. Do not "wrap" it. If two implementations exist, one must die. Consolidate to one source of truth.
-
STRICT LAYERING. Business logic belongs in
app/Services/, NEVER in controllers. Controllers handle HTTP concerns only: receive request, call service, return resource. Data access patterns go through Eloquent models with proper scopes. No "quick fixes" in the wrong layer.- Controller → receives FormRequest, calls Service, returns API Resource
- Service → contains business logic, validation beyond FormRequest, orchestration
- Model → relationships, scopes, accessors, mutators
- Job → async work dispatched by Service
-
CONTRACT-FIRST. Before implementing any module:
- Define the PHP Enum(s) in
app/Enums/for all status/type fields - Define the API Resource (response shape) first
- Define the Form Request (input validation) first
- Then implement the Service and Controller Types and contracts define behaviour. Implementation follows.
- Define the PHP Enum(s) in
-
CONSISTENCY OVER CLEVERNESS. Use the same pattern for every similar problem. If existing modules use a specific approach for pagination, error handling, or resource loading — use that exact same approach. Never introduce a "better" alternative pattern without refactoring ALL existing code to match. One pattern per problem type, across the entire codebase.
-
SINGLE SOURCE OF TRUTH. Every piece of information exists in exactly one place:
- Enum values → PHP Enum class (not string literals)
- Validation rules → Form Request (not duplicated in frontend)
- Response shape → API Resource (not ad-hoc arrays)
- Config values → .env / config files (not hardcoded)
- Schema definition → SCHEMA.md (not guessed)
Code quality
-
NO TODO / FIXME / HACK. Zero tolerance. If you cannot complete something, stop and report it. Do not leave stubs, placeholders, or "implement later" comments. Every file you touch must be production-ready.
-
NO UNTYPED CODE. PHP:
declare(strict_types=1)on every file. Return types on all methods. Typed properties. Use PHP Enums (not string literals) for all status, type, and role fields. Nomixedwhere a concrete type or union type would work. -
NO GENERIC NAMES. Names must be specific and self-documenting:
- ❌
DataService,Helper,Manager,handleData(),processItem() - ✅
ShiftAssignmentService,VolunteerAvailabilityChecker,resolveShiftConflict(),calculateFillRate()
- ❌
-
NO SILENT ERROR HANDLING. No empty catch blocks. No
catch { return null; }. Every error must be: logged (viaLog::error()with context), or rethrown, or handled with a proper API error response. Usereport($e)for unexpected errors.
Testing
-
TESTS ARE DESIGN, NOT AFTERTHOUGHT. Tests define expected behaviour. Every controller needs feature tests covering:
- 200/201 (happy path for each action)
- 401 (unauthenticated access)
- 403 (wrong organisation — cross-tenant access attempt)
- 403 (insufficient role/permission)
- 422 (validation errors with specific field assertions)
- Edge cases specific to the module (e.g., shift conflict, capacity full)
Run
php artisan testafter EVERY module. Fix failures before proceeding. Never skip, comment out, or mark tests as incomplete.
-
FACTORIES ARE REALISTIC. Use realistic Dutch test data (names, addresses, company names) that reflects actual usage. Factories must create valid, complete records — no missing required fields or placeholder values.
Data & persistence
-
DATA MODEL IS SACRED. Never deviate from SCHEMA.md. Every column, every constraint, every index documented there must be in the migration. If SCHEMA.md is unclear, ask — do not guess.
-
EVERY MIGRATION HAS down(). The
down()method must cleanly reverse theup(). Drop tables, remove columns, restore previous state. Nodown()methods that throw or do nothing. -
INDEXES ARE MANDATORY. Every foreign key column, every column used in WHERE/ORDER BY clauses, every unique constraint from SCHEMA.md must have an explicit index. Verify composite indexes match the documented patterns.
Security & multi-tenancy
-
NO UNSCOPED QUERIES. Every query on event-related models must be scoped to
organisation_idviaOrganisationScope. Mental test: can User A from Org 1 ever see, modify, or infer the existence of data from Org 2? If yes → security bug → fix immediately. -
POLICIES ARE COMPLETE. Every policy method checks:
- Does the user belong to the correct organisation?
- Does the user have the required Spatie role/permission for this action?
- No
return trueplaceholders. No missing methods.
-
FORM REQUESTS ARE COMPLETE. Every store/update has a Form Request with full validation matching SCHEMA.md constraints: required, nullable, max length, enum values (referencing the PHP Enum), exists rules with proper scoping (e.g.,
exists:events,idscoped to organisation).
API responses
-
NO BARE MODEL RETURNS. Every API response goes through an API Resource. Never
return $model,$model->toArray(), or raw arrays. Resources define the public contract. Computed fields (fill_rate, status_label, slot counts) belong in the Resource. -
CONSISTENT RESPONSE STRUCTURE. Follow the existing pattern:
{ data: {...}, meta: {...} }for paginated lists. Consistent error format with{ message: "...", errors: {...} }for validation failures.
Resilience & operations
-
IDEMPOTENT OPERATIONS. Every queued job must be safe to retry. Check-before-act: verify state hasn't changed. Use database transactions for multi-step mutations. Queued notifications and external API calls (Zender SMS/WhatsApp) must handle duplicates gracefully.
-
OBSERVABILITY. Log significant business events using
spatie/laravel-activitylog(already installed). Every create, update, delete, and status change on business entities must be logged with:causedBy($user)— who did itperformedOn($model)— what was affectedwithProperties([...])— relevant context (old values, new values)
-
API VERSIONING. All routes under
/api/v1/. Controllers inApp\Http\Controllers\Api\V1\. Never introduce breaking changes to existing endpoints — add new fields, don't rename or remove.
Process
-
MODULE GENERATION ORDER. Always follow this sequence. No skipping.
- PHP Enum(s) for status/type fields (
app/Enums/) - Migration(s) — verify against SCHEMA.md
- Eloquent Model with: HasUlids, HasFactory, SoftDeletes (if documented), OrganisationScope (if event-related), relationships, scopes, accessors
- Factory with realistic Dutch test data
- Service class for business logic (
app/Services/) - Policy for authorisation
- Form Request(s) for validation
- API Resource for response transformation
- Resource Controller (thin — delegates to Service)
- Routes in
api/routes/api.php - Feature tests — run them, fix failures
- Activity log integration in Service methods
- Update
/dev-docs/API.mdwith new routes
- PHP Enum(s) for status/type fields (
-
GIT. Auto-commit after each completed module:
feat(module-name): add backend scaffold with tests
VERIFICATION CHECKLIST (run before reporting "done")
# All tests pass
php artisan test
# Database rebuilds cleanly
php artisan migrate:fresh --seed
# New routes are visible
php artisan route:list --path=api/v1
# No forbidden patterns
grep -rn "TODO\|FIXME\|HACK\|dd(\|dump(\|var_dump\|Model::all()" \
api/app/ api/tests/ --include="*.php"
# No UUID v4 in migrations
grep -rn "uuid(" api/database/migrations/ --include="*.php"
# Static analysis (if configured)
./vendor/bin/phpstan analyse
Manual verification:
- Every new model has: HasUlids, HasFactory, correct SoftDeletes, complete $fillable, all relationships from SCHEMA.md
- Every new model with event data has OrganisationScope
- Every controller action is covered by a Policy method
- Every Service method logs activity via spatie/laravel-activitylog
- Every Form Request references PHP Enums (not string literals) for enum validation
- Business logic is in Service classes, not in Controllers
- API Resource includes computed fields where applicable
- No N+1: index actions use
with()for all accessed relationships /dev-docs/API.mdupdated with new routes
TASK
[INSERT SPECIFIC TASK HERE]