Files
crewli/CLAUDE.md
bert.hausmans 7542808cab chore: install larastan at level 6 with accept-all baseline
Installs larastan/larastan ^3.0 (v3.9.6) as a dev-dependency. Level
6 is the starting target — catches missing typehints, method-
existence, null-safety, and model-property existence. Level 8
deferred to a follow-up sprint after level-6 baseline reaches zero.

Baseline error count at install: 1556 errors across 678 analysed
files (41 distinct identifiers).

Top 10 identifiers (errors / files):
  613 /  87  property.notFound
  289 /  52  missingType.generics
  154 /  31  argument.templateType
   98 /  61  missingType.iterableValue
   77 /  32  argument.type
   50 /  26  method.notFound
   35 /  35  method.childReturnType
   32 /   9  method.unresolvableReturnType
   31 /  10  assign.propertyType
   28 /  17  instanceof.alwaysTrue

Composer scripts:
  - composer analyse              — run static analysis
  - composer analyse:baseline     — regenerate baseline
  - composer analyse:clear-cache  — clear PHPStan result cache

Config deviation from plan: checkGenericClassInNonGenericObjectType
was removed in PHPStan 2.x (which Larastan 3 bundles) — setting
dropped from phpstan.neon, otherwise config matches the work
package verbatim. Defaults cover the original intent.

Documentation: /dev-docs/LARASTAN.md added; CLAUDE.md quality-gates
section introduced (with PHPUnit + Pint + Larastan listed).

Backlog: /dev-docs/BACKLOG.md gets 10 per-identifier reduction
sprints (TECH-LARASTAN-01..10) seeded from the actual baseline top
categories, plus TECH-LARASTAN-CI and TECH-LARASTAN-L8 follow-ups.

Memory limit 2G (baseline generation completed within it).

No production behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 03:46:27 +02:00

11 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: /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.

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

  • 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)

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:

# 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/:
    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: ![Beschrijving](./images/placeholder.png)
  • 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