- 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
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:
PersonCheckedInShiftFillRateChangedArtistCheckInStatusChangedAdvanceSectionSubmittedAccreditationItemHandedOutBriefingSendQueued
Source: EventCrew Design Document v1.3, March 2026