- 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
450 lines
18 KiB
Markdown
450 lines
18 KiB
Markdown
# 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
|
|
|
|
```json
|
|
{
|
|
"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*
|