# Crewli — Claude Code Instructions > See `dev-docs/CLAUDE_CODE_TOOLING.md` for the deterministic guard-rail layer (hooks, subagent, slash commands). ## 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: `/dev-docs/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) ## Quality gates - `php artisan test` — PHPUnit feature suite. See Testing rules below. - `./vendor/bin/pint` — code style. Runs pre-commit in scope; format before merge. - `composer analyse` — Larastan static analysis at level 6 with accept-all baseline. New errors beyond the baseline must be fixed before merge. See `/dev-docs/LARASTAN.md`. - `composer rector` — Rector dry-run for modernisation suggestions. See `/dev-docs/RECTOR.md`. Apply only in scoped sprints, never automatically. - ts-reset patches TypeScript's loosest default types in both SPAs. See `/dev-docs/FRONTEND-TOOLING.md`. New TypeScript code adheres to ts-reset's stricter types automatically. - Vitest — `apps/portal` has 113+ tests; `apps/app` currently has no Vitest setup (tracked as TECH-APP-VITEST, must close before S3b lands). ## Development tooling - Laravel Telescope at `/telescope` — queries, jobs, mails, redis, events. Local + testing only, never production. super_admin role required to view. See `/dev-docs/TELESCOPE.md`. ## Repository layout - `api/` — Laravel backend - `apps/app/` — Organizer SPA (main product app + Platform Admin for super admins) - `apps/portal/` — External portal (volunteers, artists, suppliers, etc.) ## Apps and portal architecture - `apps/app/` — Organizer: event management per organisation. Includes **Platform Admin** section (`/platform/*`) for super_admin users (organisation management, user management, impersonation, activity log). - `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 two frontend origins in both Laravel (`config/cors.php` via env) and the Vite dev server proxy: - app: `localhost:5174` - portal: `localhost:5175` **Production (`crewli.app`):** API `https://api.crewli.app`, SPAs `https://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 Crewli uses **MySQL 8.0 exclusively** across all environments: - **Local development**: MySQL 8.0 via `docker-compose.yml` (`bm_mysql` container, port 3306). Run `make services` to start. - **Testing**: MySQL 8.0 via the same Docker container, separate `crewli_test` database. Run `make test-db-create` once, then `make test`. - **Staging/production**: MySQL 8.0 on the deployment VPS. **SQLite is forbidden** in all environments. Reasons: - SQLite has known quirks around foreign-key constraints (rebuild-on-FK-add cascades), JSON queries (key-order non-determinism on round-trip differs from MySQL), and concurrent writes that mask bugs which MySQL would surface in production - Cross-database query syntax differences cause silent test/production drift (e.g. `sqlite_master` vs. `information_schema`, VARCHAR length enforcement) - Crewli's enterprise-grade product philosophy doesn't tolerate "but it works on SQLite" as a passing test If a contributor finds SQLite references in `phpunit.xml`, `.env.testing`, or any config file, treat it as a regression and fix immediately. When writing migrations or queries: - Use Laravel's query builder where possible — it handles cross-DB syntax consistently - For raw SQL, write MySQL syntax (no need for SQLite fallbacks). Index introspection: query `information_schema.STATISTICS`, never `sqlite_master`. - Foreign keys go on every relation column. The `nullOnDelete` / `cascadeOnDelete` choice is per the relationship's domain semantics; "no FK" is not an option. - JSON values stored in byte-stable columns (`schema_snapshot`, webhook `payload_snapshot`, activity-log `properties` for diff payloads) MUST be canonicalized at write via `App\Support\Json\JsonCanonicalizer`. MySQL JSON columns may reorder associative-array keys on round-trip; canonicalize so re-emits / HMAC signatures / audit-replay diffs are byte-identical. Opaque-config JSON (`form_schemas.settings`, `translations`) is exempt — key order has no semantic meaning there. Tests on canonicalized data use `assertSame(JsonCanonicalizer::encode($a), JsonCanonicalizer::encode($b))`. Other database rules: - 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) ### Schema dumps (CI fast path) Laravel uses `database/schema/{driver}-schema.sql` as a fast-path baseline that `migrate:fresh` loads in one statement (~1s) instead of replaying every migration (~6s × N migrations). For Crewli's backfill / migration tests that call `migrate:fresh` per test, this drops the 4 backfill-test classes from ~128s to ~28s combined (78% reduction). The dump is **committed** at `api/database/schema/mysql-schema.sql` and is the default code path. CI loads it automatically via `migrate:fresh`. **One-time host setup** (required for both generating and loading the dump): ```bash brew install mysql-client echo 'export PATH="/opt/homebrew/opt/mysql-client/bin:$PATH"' >> ~/.zshrc source ~/.zshrc ``` Verify with `which mysql && which mysqldump`. Both must resolve to a real binary; Laravel's `schema:dump` and `migrate:fresh` schema-load both shell out to these. **After adding a new migration:** ```bash make schema-dump git add api/database/schema/mysql-schema.sql ``` `make schema-dump` brings `crewli_test` to head and runs `php artisan schema:dump`. Commit the regenerated file in the same PR as the migration. `--prune` is NOT used: individual migration files stay readable in `api/database/migrations/` for audit / rollback purposes. ### 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) ### Vuexy reference source (mandatory) When referencing Vuexy demo pages, components, or patterns, ALWAYS use the TypeScript Vue version at: ``` resources/vuexy-admin-v10.11.1/vue-version/typescript-version/full-version/ ``` This is the **ONLY** valid reference path. Never use: - `javascript-version/` — wrong language - `starter-kit/` — incomplete, missing components - Any other variant or version Before implementing any Vuexy-based page or component, read the reference implementation from this path first: ```bash # Example: find auth page references find resources/vuexy-admin-v10.11.1/vue-version/typescript-version/full-version/src/pages -name "*.vue" | grep -i "login\|auth" ``` ### Vuexy-first strategy Before writing ANY frontend component, consult `/dev-docs/VUEXY_COMPONENTS.md` and follow this decision tree: 1. **Can a standard Vuetify component do this?** → Use it with default props. Do not wrap it in a custom component. 2. **Does Vuexy provide an @core component for this?** → Use it. Check `/dev-docs/VUEXY_COMPONENTS.md` section 1 for the full registry. 3. **Does an existing Crewli page already solve a similar UI pattern?** → Copy that pattern exactly. Check `/dev-docs/VUEXY_COMPONENTS.md` section 3 for established patterns and their reference implementations. 4. **None of the above?** → Only then write custom code. Add `