Session 2.6's per-test migrate:fresh in 4 backfill test classes replays ~120 migrations. Laravel's schema:dump produces a single SQL file that migrate:fresh loads atomically when present — significantly faster on environments where the host has the \`mysql\` CLI available for Laravel's load step. Changes: - New Makefile target \`schema-dump\` runs mysqldump INSIDE the bm_mysql Docker container (no host mysqldump dependency to GENERATE the dump). Outputs api/database/schema/mysql-schema.sql. - api/database/schema/.gitignore added: mysql-schema.sql is NOT committed by default. Laravel's auto-load on migrate:fresh shells out to the host's \`mysql\` CLI; on hosts without it, the presence of the dump file actively breaks migrate (exit 127). Treat the dump as opt-in per dev environment. - CLAUDE.md "Schema dumps (opt-in fast path)" subsection added with the workflow: brew install mysql-client → migrate to head → make schema-dump → optionally commit. phpstan-baseline.neon: removed a stale "unused use \$actor" entry in FormSubmissionService whose underlying closure pint cleaned up in commit060d6f3(Task 1). Wall-time on this dev machine (no host mysql CLI): NO speedup, backfill tests still ~6s per setUp. The dump file is ready for environments that have mysql CLI. Documented as a deviation. JSON canonicalization (commit060d6f3) is the load-bearing correctness fix from this branch; the schema-dump perf path is a nice-to-have that activates per-environment. Refs: WS-6 session 2.6 deviation #5 cleanup Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
340 lines
16 KiB
Markdown
340 lines
16 KiB
Markdown
# 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: `/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 (opt-in fast path)
|
|
|
|
Laravel supports `database/schema/{driver}-schema.sql` as a fast-path
|
|
baseline that `migrate:fresh` loads in one statement instead of replaying
|
|
every migration. For Crewli's backfill / migration tests (which call
|
|
`migrate:fresh` per test), this can yield a meaningful speedup —
|
|
**when the host has the `mysql` and `mysqldump` CLI tools available.**
|
|
|
|
Workflow:
|
|
|
|
1. Install MySQL client tools on host (one-time):
|
|
```bash
|
|
brew install mysql-client
|
|
echo 'export PATH="/opt/homebrew/opt/mysql-client/bin:$PATH"' >> ~/.zshrc
|
|
```
|
|
2. Bring `crewli_test` to head: `DB_DATABASE=crewli_test php artisan migrate --force`
|
|
3. Generate the dump: `make schema-dump`
|
|
4. Commit `api/database/schema/mysql-schema.sql` alongside any new migrations.
|
|
|
|
The schema dump is **NOT committed by default** because Laravel's
|
|
auto-load path shells out to the `mysql` CLI; on hosts without it,
|
|
the presence of the dump file actively breaks `migrate` / `migrate:fresh`
|
|
(load fails, exit 127). Treat the dump as opt-in per dev environment.
|
|
|
|
`make schema-dump` runs `mysqldump` inside the `bm_mysql` Docker
|
|
container, so contributors don't need `mysqldump` on the host to
|
|
**generate** the dump — only to **load** it via Laravel's auto-detection.
|
|
|
|
`--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 `<style scoped>`
|
|
with a comment explaining why Vuexy/Vuetify couldn't handle it.
|
|
|
|
Concrete component rules:
|
|
- Tables: `v-data-table-server` with server-side pagination — never client-side for API data
|
|
- Cards: `v-card` directly, or `AppCardActions` when collapse/refresh/remove is needed
|
|
- Forms in dialogs: `v-dialog` + `v-card` + `v-form` — follow the established dialog pattern
|
|
- Detail panels: `v-navigation-drawer` with `temporary` and `location="end"` — follow ShiftDetailPanel pattern
|
|
- Date/time pickers: `AppDateTimePicker` from @core — never raw input[type=date]
|
|
- Status indicators: `v-chip` with color prop — never custom styled spans
|
|
- Loading states: `v-skeleton-loader` — never custom spinners
|
|
- Error states: `v-alert` with retry button — never custom error divs
|
|
- Empty states: `v-card` with icon + message + action button
|
|
- Notifications: `v-snackbar` — never custom toast components
|
|
- Page layout: `v-row` + `v-col` with Vuetify breakpoint props — never CSS grid or custom flexbox
|
|
|
|
**Before ANY frontend task:** read `/dev-docs/VUEXY_COMPONENTS.md` to verify
|
|
you are using available components rather than building custom ones.
|
|
|
|
### 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/api/use[Module].ts`
|
|
- `src/lib/axios.ts` is the canonical axios instance — the only place axios is imported directly
|
|
- Use Pinia stores for cross-component state — no prop drilling
|
|
|
|
### TypeScript
|
|
|
|
- No `any` types — ever. Every variable, prop, emit, return type, ref, computed must be fully typed
|
|
- Types first: create `src/types/[module].ts` before composables or components
|
|
- Mirror backend PHP Enums as `as const` objects in `src/types/`:
|
|
```typescript
|
|
export const ShiftStatus = { PENDING: 'pending', APPROVED: 'approved' } as const
|
|
export type ShiftStatus = typeof ShiftStatus[keyof typeof ShiftStatus]
|
|
```
|
|
|
|
### Forms
|
|
|
|
- VeeValidate for form state + Zod for schema validation — always together
|
|
- Zod schemas must mirror the backend Form Request rules (field names, required/optional, types)
|
|
- No inline validation logic in components
|
|
|
|
### 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
|
|
- **Three states per page:** every data-driven view must handle loading (skeleton/spinner), error (`v-alert` with retry button), and empty (helpful message with action button)
|
|
- Use Vuetify responsive props (`cols`, `sm`, `md`, `lg`) — no fixed pixel widths
|
|
- Custom CSS via `<style scoped>` only as last resort when no Vuetify utility exists
|
|
|
|
## 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`)
|
|
- Never: TypeScript `any` type (use proper types, generics, or `unknown` with type guards)
|
|
- Never: import axios directly in a component (use `src/lib/axios.ts` via a composable)
|
|
|
|
## 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. TypeScript types in `src/types/[module].ts`
|
|
11. API composable in `src/composables/api/use[Module].ts`
|
|
12. Pinia store in `src/stores/use[Module]Store.ts` (only if cross-component state is needed)
|
|
13. Vue page component in `src/pages/[module]/`
|
|
14. Add route in Vue Router
|
|
|
|
## User Documentation (VitePress)
|
|
|
|
End-user documentation lives in /docs/ as a VitePress site.
|
|
Developer documentation (SCHEMA.md, API.md, etc.) lives in /dev-docs/.
|
|
|
|
### When to write docs
|
|
|
|
When completing a feature that introduces or changes **user-facing behaviour**,
|
|
create or update the corresponding documentation page under /docs/.
|
|
|
|
### How to write docs
|
|
|
|
1. Read /docs/.templates/style-guide.md for terminology, tone, and structure rules
|
|
2. Use /docs/.templates/feature-page.md or concept-page.md as your starting template
|
|
3. Every page MUST have frontmatter with: title, description, tags
|
|
4. Use Dutch language for all content (informal "je/jij")
|
|
5. Use Crewli terminology from the style guide — never English equivalents in user docs
|
|
|
|
### What to include
|
|
|
|
- What the feature does (2-3 sentences)
|
|
- Step-by-step instructions from the user's perspective
|
|
- Which roles have access (table format)
|
|
- Screenshot placeholders where visual guidance helps: ``
|
|
- Links to related pages
|
|
|
|
### File placement
|
|
|
|
Match the existing structure under /docs/:
|
|
|
|
- Organizer features → /docs/organizer/[category]/
|
|
- Volunteer features → /docs/volunteer/
|
|
- Portal features → /docs/portal/
|
|
- General concepts → /docs/guide/
|
|
|
|
## Git Commit Policy
|
|
|
|
**After every successful task completion, commit the changes immediately.**
|
|
|
|
Rules:
|
|
1. After all tests pass and build succeeds, stage and commit the changed files
|
|
2. Use conventional commit messages: `feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `chore:`
|
|
3. One commit per logical unit of work (one feature, one bugfix, one refactor)
|
|
4. Never bundle unrelated changes in a single commit
|
|
5. Never commit with failing tests
|
|
6. Do NOT push automatically — only commit locally. The developer will push manually.
|
|
|
|
Commit message format:
|
|
```
|
|
feat: short description of the feature
|
|
fix: short description of the bug fix
|
|
refactor: short description of the refactoring
|
|
docs: short description of documentation changes
|
|
test: short description of test additions
|
|
```
|
|
|
|
Examples:
|
|
- `feat: person tags system with org-level skills and sync endpoint`
|
|
- `fix: auth race condition on page refresh`
|
|
- `docs: update SCHEMA.md with person_identity_matches table`
|