Files
band-management/.cursor/ARCHITECTURE.md
bert.hausmans 1cb7674d52 refactor: align codebase with EventCrew domain and trim legacy band stack
- Update API: events, users, policies, routes, resources, migrations
- Remove deprecated models/resources (customers, setlists, invitations, etc.)
- Refresh admin app and docs; remove apps/band

Made-with: Cursor
2026-03-29 23:19:06 +02:00

18 KiB

EventCrew - Architecture

Multi-tenant SaaS platform for event- and festival management. Source of truth: /resources/design/EventCrew_Design_Document_v1.3.docx

System Overview

┌─────────────────────────────────────────────────────────────────────────┐
│                              INTERNET                                   │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
        ┌───────────────────────────┼───────────────────────────┐
        │                           │                           │
        ▼                           ▼                           ▼
┌───────────────┐           ┌───────────────┐           ┌───────────────┐
│  Admin SPA    │           │  Organizer    │           │  Portal SPA   │
│  (Super Admin)│           │  SPA (Main)   │           │  (External)   │
│  :5173        │           │  :5174        │           │  :5175        │
└───────┬───────┘           └───────┬───────┘           └───────┬───────┘
        │                           │                           │
        └───────────────────────────┼───────────────────────────┘
                                    │ CORS + Sanctum tokens
                                    ▼
                        ┌───────────────────────┐
                        │   Laravel 12 REST API │
                        │   (JSON only, no      │
                        │    Blade views)        │
                        │   :8000               │
                        └───────────┬───────────┘
                                    │
                    ┌───────────────┼───────────────┐
                    │               │               │
                    ▼               ▼               ▼
            ┌───────────┐   ┌───────────┐   ┌───────────┐
            │   MySQL 8 │   │   Redis   │   │  Mailpit  │
            │   :3306   │   │   :6379   │   │   :8025   │
            └───────────┘   └───────────┘   └───────────┘

Golden Rule: Laravel is exclusively a JSON REST API. No Blade views, no Mix, no Inertia. Every response is application/json. Vue handles ALL UI via three SPAs.


Applications

Admin Dashboard (apps/admin/)

Purpose: Super Admin platform management.

Users: Platform owner only (super_admin role).

Features:

  • Organisation management (CRUD, billing status)
  • Platform user management
  • Global settings

Vuexy Version: typescript-version/full-version


Organizer App (apps/app/)

Purpose: Main application for event management per organisation.

Users: Organisation Admins, Event Managers, Staff Coordinators, Artist Managers, Volunteer Coordinators.

Features:

  • Event lifecycle management (Draft through Closed)
  • Festival Sections, Time Slots, Shift planning
  • Person & Crowd management (Crew, Volunteers, Artists, Guests, Press, Partners, Suppliers)
  • Accreditation engine (categories, items, access zones)
  • Artist booking & advancing
  • Timetable & stage management
  • Briefing builder & communication hub
  • Mission Control (show day operations)
  • Form builder with conditional logic
  • Supplier & production management
  • Reporting & insights

Vuexy Version: typescript-version/full-version (customized navigation)


Portal (apps/portal/)

Purpose: External-facing portal with two access modes.

Users: Volunteers, Crew (login-based), Artists, Suppliers, Press (token-based).

Access Modes:

User Access Mode Rationale
Volunteer / Crew Login (auth:sanctum) Long-term relationship, festival passport, shift history
Artist / Tour Manager Token (portal.token middleware) Per-event, advance portal via signed URL
Supplier / Partner Token (portal.token middleware) Per-event, production request via token
Press / Media Token (portal.token middleware) Per-event accreditation, no recurring relationship

Router guard logic: If route.query.token -> token mode. If authStore.isAuthenticated -> login mode. Otherwise -> redirect to /login.

Vuexy Version: typescript-version/starter-kit (stripped: no sidebar, no customizer, no dark mode toggle; uses Vuetify components + Vuexy SCSS)


Multi-Tenant Data Model

Shared database schema with organisation scoping on all tables. No row-level security at DB level; scoping enforced via Laravel Policies and Eloquent Global Scopes.

Scoping Rule: EVERY query on event data MUST have an organisation_id scope via OrganisationScope Global Scope.

Tenancy Hierarchy:

Platform (Super Admin)
  └─ Organisation (client A)
       └─ Event (event 1)
            └─ Festival Section (Bar, Hospitality, Technical, ...)
                 ├─ Time Slots (DAY1-EARLY-CREW, DAY1-EARLY-VOLUNTEER, ...)
                 └─ Shifts (Bar x DAY1-EARLY-VOLUNTEER, 5 slots)

Three-Level Role & Permission Model

Managed via Spatie laravel-permission with team-based permissions.

Level Scope Roles Implementation
App Level Whole platform super_admin, support_agent Spatie role
Organisation Level Within one org org_admin, org_member, org_readonly Spatie team = organisation
Event Level Within one event event_manager, artist_manager, staff_coordinator, volunteer_coordinator, accreditation_officer event_user_roles pivot table

Middleware: OrganisationRoleMiddleware and EventRoleMiddleware check per route.


Event Lifecycle

Phase Description
draft Created but not published. Only admin sees it.
published Active in planning. Internal modules available. External portals closed.
registration_open Volunteer registration and artist advance portals open.
buildup Setup days. Crew shifts begin. Accreditation distribution starts.
showday Active event days. Mission Control active. Real-time check-in.
teardown Breakdown days. Inventory return. Shift closure.
closed Event completed. Read-only. Reports available.

API Structure

Base URL

  • Development: http://localhost:8000/api/v1

Route Groups

# Public (no auth)
POST   /auth/login
POST   /portal/token-auth              Token-based portal access
POST   /portal/form-submit             Public form submission

# Protected (auth:sanctum)
POST   /auth/logout
GET    /auth/me                        Returns user + organisations + event roles

# Organisations
GET/POST   /organisations
GET/PUT    /organisations/{id}
POST       /organisations/{id}/invite
GET        /organisations/{id}/members

# Events (nested under organisations)
GET/POST   /organisations/{org}/events
GET/PUT/DELETE /events/{id}
PUT        /events/{id}/status

# Festival Sections
GET/POST   /events/{event}/sections
GET/PUT/DELETE /sections/{id}
GET        /sections/{id}/dashboard

# Time Slots
GET/POST   /events/{event}/time-slots
PUT/DELETE /time-slots/{id}

# Shifts
GET/POST   /sections/{section}/shifts
PUT/DELETE /shifts/{id}
POST       /shifts/{id}/assign
POST       /shifts/{id}/claim           Volunteer self-service

# Persons
GET/POST   /events/{event}/persons
GET/PUT    /persons/{id}
POST       /persons/{id}/approve
POST       /persons/{id}/checkin

# Crowd Types & Lists
GET/POST   /organisations/{org}/crowd-types
GET/POST   /events/{event}/crowd-lists

# Artists & Advancing
GET/POST   /events/{event}/artists
GET/PUT    /artists/{id}
GET/POST   /artists/{id}/sections       Advance sections
POST       /sections/{id}/submit        Advance submission

# Accreditation
GET/POST   /events/{event}/accreditation-items
POST       /persons/{id}/accreditations
GET/POST   /events/{event}/access-zones

# Briefings & Communication
GET/POST   /events/{event}/briefings
POST       /briefings/{id}/send
GET/POST   /events/{event}/campaigns
POST       /campaigns/{id}/send

# Mission Control
GET        /events/{event}/mission-control
POST       /persons/{id}/checkin-item

# Scanners & Inventory
GET/POST   /events/{event}/scanners
POST       /scan
GET/POST   /events/{event}/inventory

# Reports
GET        /events/{event}/reports/{type}

# Portal (token-based, portal.token middleware)
GET        /portal/artist
POST       /portal/advancing
GET        /portal/supplier
POST       /portal/production-request

API Response Format

{
  "data": { ... },
  "meta": {
    "pagination": {
      "current_page": 1,
      "per_page": 15,
      "total": 100,
      "last_page": 7
    }
  }
}

Core Database Schema

Primary Keys: ULID on all business tables via HasUlids trait. Pure pivot tables use auto-increment integer PK.

Soft Deletes ON: organisations, events, festival_sections, shifts, shift_assignments, persons, artists, companies, production_requests.

Soft Deletes OFF (audit records): check_ins, briefing_sends, message_replies, shift_waitlist, volunteer_festival_history.

JSON Columns: ONLY for opaque config (blocks, fields, settings, items). NEVER for dates, status values, foreign keys, booleans, or anything filtered/sorted/aggregated.

1. Foundation

Table Key Columns Notes
users id (ULID), name, email, password, timezone, locale, avatar, deleted_at Platform-wide, unique per email.
organisations id (ULID), name, slug, billing_status, settings (JSON: display prefs only), deleted_at hasMany events, crowd_types.
organisation_user id (int AI), user_id, organisation_id, role Pivot. Integer PK.
user_invitations id (ULID), email, invited_by_user_id, organisation_id, event_id (nullable), role, token (ULID unique), status, expires_at INDEX: (token), (email, status).
events id (ULID), organisation_id, name, slug, start_date, end_date, timezone, status (enum), deleted_at INDEX: (organisation_id, status).
event_user_roles id (int AI), user_id, event_id, role Pivot. Integer PK.

2. Locations

Table Key Columns Notes
locations id (ULID), event_id, name, address, lat, lng, description, access_instructions INDEX: (event_id).

3. Festival Sections, Time Slots & Shifts

Table Key Columns Notes
festival_sections id (ULID), event_id, name, sort_order, deleted_at INDEX: (event_id, sort_order).
time_slots id (ULID), event_id, name, person_type (CREW/VOLUNTEER/PRESS/...), date, start_time, end_time INDEX: (event_id, person_type, date).
shifts id (ULID), festival_section_id, time_slot_id, location_id, slots_total, slots_open_for_claiming, status, deleted_at INDEX: (festival_section_id, time_slot_id).
shift_assignments id (ULID), shift_id, person_id, time_slot_id (denormalized), status (pending_approval/approved/rejected/cancelled/completed), auto_approved, deleted_at UNIQUE(person_id, time_slot_id).
volunteer_availabilities id (ULID), person_id, time_slot_id, submitted_at UNIQUE(person_id, time_slot_id).
shift_waitlist id (ULID), shift_id, person_id, position, added_at UNIQUE(shift_id, person_id).
shift_swap_requests id (ULID), from_assignment_id, to_person_id, status, auto_approved
shift_absences id (ULID), shift_assignment_id, person_id, reason, status

4. Volunteer Profile & History

Table Key Columns Notes
volunteer_profiles id (ULID), user_id (unique), bio, tshirt_size, first_aid, driving_licence, reliability_score (0.00-5.00) Platform-wide, 1:1 with users.
volunteer_festival_history id (ULID), user_id, event_id, hours_planned, hours_completed, no_show_count, coordinator_rating, would_reinvite UNIQUE(user_id, event_id). Never visible to volunteer.
post_festival_evaluations id (ULID), event_id, person_id, overall_rating, would_return, feedback_text
festival_retrospectives id (ULID), event_id (unique), KPI columns, top_feedback (JSON)

5. Crowd Types, Persons & Crowd Lists

Table Key Columns Notes
crowd_types id (ULID), organisation_id, name, system_type (CREW/GUEST/ARTIST/VOLUNTEER/PRESS/PARTNER/SUPPLIER), color, icon Org-level config.
persons id (ULID), user_id (nullable), event_id, crowd_type_id, company_id (nullable), name, email, phone, status, is_blacklisted, custom_fields (JSON), deleted_at user_id nullable for externals. UNIQUE(event_id, user_id) WHERE user_id IS NOT NULL.
companies id (ULID), organisation_id, name, type, contact_*, deleted_at Shared across events within org.
crowd_lists id (ULID), event_id, crowd_type_id, name, type (internal/external), auto_approve, max_persons
crowd_list_persons id (int AI), crowd_list_id, person_id Pivot.

6. Accreditation Engine

Table Key Columns Notes
accreditation_categories id (ULID), organisation_id, name, sort_order, icon Org-level.
accreditation_items id (ULID), accreditation_category_id, name, is_date_dependent, barcode_type, cost_price Org-level items.
event_accreditation_items id (ULID), event_id, accreditation_item_id, max_quantity_per_person, is_active Activates item per event. UNIQUE(event_id, accreditation_item_id).
accreditation_assignments id (ULID), person_id, accreditation_item_id, event_id, date, quantity, is_handed_out
access_zones id (ULID), event_id, name, zone_code (unique per event)
access_zone_days id (int AI), access_zone_id, day_date UNIQUE(access_zone_id, day_date).
person_access_zones id (int AI), person_id, access_zone_id, valid_from, valid_to

7. Artists & Advancing

Table Key Columns Notes
artists id (ULID), event_id, name, booking_status (concept/requested/option/confirmed/contracted/cancelled), portal_token (ULID unique), deleted_at
performances id (ULID), artist_id, stage_id, date, start_time, end_time, check_in_status INDEX: (stage_id, date, start_time).
stages id (ULID), event_id, name, color, capacity
stage_days id (int AI), stage_id, day_date UNIQUE(stage_id, day_date).
advance_sections id (ULID), artist_id, name, type, is_open, sort_order
advance_submissions id (ULID), advance_section_id, data (JSON), status
artist_contacts id (ULID), artist_id, name, email, role
artist_riders id (ULID), artist_id, category (technical/hospitality), items (JSON)

8. Communication & Briefings

Table Key Columns Notes
briefing_templates id (ULID), event_id, name, type, blocks (JSON)
briefings id (ULID), event_id, briefing_template_id, name, target_crowd_types (JSON), status
briefing_sends id (ULID), briefing_id, person_id, status (queued/sent/opened/downloaded) NO soft delete.
communication_campaigns id (ULID), event_id, type (email/sms/whatsapp), status
messages id (ULID), event_id, sender_user_id, recipient_person_id, urgency (normal/urgent/emergency)
broadcast_messages id (ULID), event_id, sender_user_id, body, urgency

9. Forms, Check-In & Operational

Table Key Columns Notes
public_forms id (ULID), event_id, crowd_type_id, fields (JSON), conditional_logic (JSON), iframe_token
form_submissions id (ULID), public_form_id, person_id, data (JSON)
check_ins id (ULID), event_id, person_id, scanned_by_user_id, scanned_at NO soft delete. Immutable audit record.
scanners id (ULID), event_id, name, type, pairing_code
inventory_items id (ULID), event_id, name, item_code, assigned_to_person_id
production_requests id (ULID), event_id, company_id, title, status, token (ULID unique)
material_requests id (ULID), production_request_id, category, name, quantity, status

Model Relationships

User

  • belongsToMany Organisations (via organisation_user)
  • belongsToMany Events (via event_user_roles)

Organisation

  • hasMany Events
  • hasMany CrowdTypes
  • hasMany AccreditationCategories
  • hasMany Companies
  • belongsToMany Users (via organisation_user)

Event

  • belongsTo Organisation
  • hasMany FestivalSections
  • hasMany TimeSlots
  • hasMany Persons
  • hasMany Artists
  • hasMany Briefings
  • hasMany Locations
  • hasMany AccessZones
  • hasMany PublicForms

FestivalSection

  • belongsTo Event
  • hasMany Shifts

TimeSlot

  • belongsTo Event
  • hasMany Shifts
  • hasMany ShiftAssignments (denormalized)

Shift

  • belongsTo FestivalSection
  • belongsTo TimeSlot
  • belongsTo Location (nullable)
  • hasMany ShiftAssignments

Person

  • belongsTo Event
  • belongsTo CrowdType
  • belongsTo User (nullable)
  • belongsTo Company (nullable)
  • hasMany ShiftAssignments
  • hasMany AccreditationAssignments
  • hasMany CheckIns

Artist

  • belongsTo Event
  • hasMany Performances
  • hasMany AdvanceSections
  • hasMany ArtistContacts

Security & CORS

Three frontend origins in config/cors.php (via env):

App Dev URL Env Variable
Admin http://localhost:5173 FRONTEND_ADMIN_URL
App http://localhost:5174 FRONTEND_APP_URL
Portal http://localhost:5175 FRONTEND_PORTAL_URL

Real-time Events (WebSocket)

Via Laravel Echo + Pusher/Soketi:

  • PersonCheckedIn
  • ShiftFillRateChanged
  • ArtistCheckInStatusChanged
  • AdvanceSectionSubmitted
  • AccreditationItemHandedOut
  • BriefingSendQueued

Source: EventCrew Design Document v1.3, March 2026