30 KiB
Band Management - Architecture
This document describes the system architecture, design decisions, and patterns used in this application.
System Overview
┌─────────────────────────────────────────────────────────────────────────┐
│ INTERNET │
└─────────────────────────────────────────────────────────────────────────┘
│
┌───────────────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Admin SPA │ │ Band SPA │ │ Customer SPA │
│ (Vuexy Full) │ │ (Vuexy Lite) │ │ (Vuexy Lite) │
│ :5173 │ │ :5174 │ │ :5175 │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└───────────────────────────┼───────────────────────────┘
│
▼
┌───────────────────────┐
│ Laravel API │
│ (Sanctum) │
│ :8000 │
└───────────┬───────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ MySQL │ │ Redis │ │ Mailpit │
│ :3306 │ │ :6379 │ │ :8025 │
└───────────┘ └───────────┘ └───────────┘
Applications
Admin Dashboard (apps/admin/)
Purpose: Full management interface for band administrators.
Users: Band leaders, managers, booking agents
Features:
- Member management (CRUD, roles, invitations)
- Event/gig management (calendar, list, RSVP tracking)
- Music catalog (songs, attachments, metadata)
- Setlist builder (drag-drop, templates)
- Location management (venues, contacts)
- Customer CRM (companies, individuals, history)
- Booking request management
- Reports and analytics
Vuexy Version: typescript-version/full-version
Band Portal (apps/band/)
Purpose: Member-facing interface for band members.
Users: Musicians, performers, crew
Features:
- Personal dashboard (upcoming gigs)
- Event calendar with RSVP
- View setlists and music
- Download attachments (lyrics, charts)
- Profile settings
- Notifications
Vuexy Version: typescript-version/starter-kit
Customer Portal (apps/customers/)
Purpose: Client-facing interface for customers.
Users: Event organizers, venue managers, clients
Features:
- View booked events
- Submit booking requests
- Track request status
- View assigned setlists (if permitted)
- Profile settings
Vuexy Version: typescript-version/starter-kit
API Structure
Base URL
- Development:
http://localhost:8000/api/v1 - Production:
https://api.bandmanagement.nl/api/v1
Authentication Endpoints
POST /auth/register Register new user
POST /auth/login Login, returns token
POST /auth/logout Logout, revokes token
GET /auth/user Get authenticated user
POST /auth/forgot-password Request password reset
POST /auth/reset-password Reset password with token
Resource Endpoints
# Events
GET /events List events (paginated, filterable)
POST /events Create event
GET /events/{id} Get event details
PUT /events/{id} Update event
DELETE /events/{id} Delete event
POST /events/{id}/invite Invite members to event
GET /events/{id}/invitations Get event invitations
POST /events/{id}/rsvp Submit RSVP response
POST /events/{id}/duplicate Duplicate event
# Members
GET /members List members
POST /members Create member
GET /members/{id} Get member details
PUT /members/{id} Update member
DELETE /members/{id} Delete/deactivate member
POST /members/invite Send invitation email
# Music
GET /music List music numbers
POST /music Create music number
GET /music/{id} Get music number
PUT /music/{id} Update music number
DELETE /music/{id} Delete music number
POST /music/{id}/attachments Upload attachment
DELETE /music/{id}/attachments/{aid} Delete attachment
# Setlists
GET /setlists List setlists
POST /setlists Create setlist
GET /setlists/{id} Get setlist with items
PUT /setlists/{id} Update setlist
DELETE /setlists/{id} Delete setlist
POST /setlists/{id}/clone Clone setlist
PUT /setlists/{id}/items Reorder/update items
# Locations
GET /locations List locations
POST /locations Create location
GET /locations/{id} Get location
PUT /locations/{id} Update location
DELETE /locations/{id} Delete location
# Customers
GET /customers List customers
POST /customers Create customer
GET /customers/{id} Get customer
PUT /customers/{id} Update customer
DELETE /customers/{id} Delete customer
# Booking Requests (Customer Portal)
GET /booking-requests List user's requests
POST /booking-requests Submit booking request
GET /booking-requests/{id} Get request details
# Notifications
GET /notifications List notifications
PUT /notifications/{id}/read Mark as read
POST /notifications/read-all Mark all as read
Database Schema
Entity Relationship Diagram
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ users │ │ customers │ │ locations │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ id (ULID) │──┐ │ id (ULID) │ │ id (ULID) │
│ name │ │ │ user_id (FK) │───────│ name │
│ email │ │ │ name │ │ address │
│ type │ │ │ company_name │ │ city │
│ role │ │ │ type │ │ capacity │
│ status │ │ │ email │ │ contact_* │
└──────────────┘ │ └──────────────┘ └──────────────┘
│ │ │ │
│ │ │ │
▼ │ ▼ ▼
┌──────────────┐ │ ┌──────────────┐ ┌──────────────┐
│event_invites │ │ │ events │───────│ setlists │
├──────────────┤ │ ├──────────────┤ ├──────────────┤
│ id (ULID) │ │ │ id (ULID) │ │ id (ULID) │
│ event_id(FK) │──┼───▶│ title │ │ name │
│ user_id (FK) │──┘ │ location_id │ │ description │
│ rsvp_status │ │ customer_id │ │ is_template │
│ rsvp_note │ │ setlist_id │◀──────│ is_archived │
└──────────────┘ │ event_date │ └──────────────┘
│ status │ │
│ created_by │ │
└──────────────┘ ▼
┌──────────────┐
┌──────────────┐ ┌──────────────┐ │setlist_items │
│music_numbers │───────│music_attach │ ├──────────────┤
├──────────────┤ ├──────────────┤ │ id (ULID) │
│ id (ULID) │ │ id (ULID) │ │ setlist_id │
│ title │ │ music_num_id │ │ music_num_id │
│ artist │ │ file_name │ │ position │
│ duration │ │ file_type │ │ set_number │
│ key, tempo │ │ file_path │ │ is_break │
│ tags (JSON) │ └──────────────┘ └──────────────┘
└──────────────┘
Table Definitions
users
| Column | Type | Description |
|---|---|---|
| id | ULID | Primary key |
| name | string | Full name |
| string | Unique email | |
| email_verified_at | timestamp | Email verification |
| password | string | Hashed password |
| phone | string? | Phone number |
| bio | text? | Biography |
| instruments | json? | Array of instruments |
| avatar_path | string? | Avatar file path |
| type | enum | member, customer |
| role | enum? | admin, booking_agent, music_manager, member |
| status | enum | active, inactive |
| invited_at | timestamp? | When invited |
| last_login_at | timestamp? | Last login |
| remember_token | string? | Remember me token |
| created_at | timestamp | Created |
| updated_at | timestamp | Updated |
customers
| Column | Type | Description |
|---|---|---|
| id | ULID | Primary key |
| user_id | ULID? | FK to users (for portal access) |
| name | string | Contact name |
| company_name | string? | Company name |
| type | enum | individual, company |
| string? | ||
| phone | string? | Phone |
| address | string? | Street address |
| city | string? | City |
| postal_code | string? | Postal code |
| country | string | Country (default: NL) |
| notes | text? | Internal notes |
| is_portal_enabled | boolean | Can access portal |
| created_at | timestamp | Created |
| updated_at | timestamp | Updated |
locations
| Column | Type | Description |
|---|---|---|
| id | ULID | Primary key |
| name | string | Venue name |
| address | string | Street address |
| city | string | City |
| postal_code | string? | Postal code |
| country | string | Country (default: NL) |
| latitude | decimal? | GPS latitude |
| longitude | decimal? | GPS longitude |
| capacity | integer? | Max capacity |
| contact_name | string? | Contact person |
| contact_email | string? | Contact email |
| contact_phone | string? | Contact phone |
| stage_specs | text? | Stage specifications |
| technical_notes | text? | Technical requirements |
| parking_info | text? | Parking information |
| notes | text? | General notes |
| created_at | timestamp | Created |
| updated_at | timestamp | Updated |
events
| Column | Type | Description |
|---|---|---|
| id | ULID | Primary key |
| title | string | Event title |
| description | text? | Description |
| location_id | ULID? | FK to locations |
| customer_id | ULID? | FK to customers |
| setlist_id | ULID? | FK to setlists |
| event_date | date | Date of event |
| start_time | time | Start time |
| end_time | time? | End time |
| load_in_time | time? | Load-in time |
| soundcheck_time | time? | Soundcheck time |
| fee | decimal(10,2)? | Payment amount |
| currency | string | Currency (default: EUR) |
| status | enum | draft, pending, confirmed, completed, cancelled |
| visibility | enum | private, members, public |
| rsvp_deadline | datetime? | RSVP deadline |
| notes | text? | Public notes |
| internal_notes | text? | Admin-only notes |
| is_public_setlist | boolean | Show setlist to customer |
| created_by | ULID | FK to users |
| created_at | timestamp | Created |
| updated_at | timestamp | Updated |
event_invitations
| Column | Type | Description |
|---|---|---|
| id | ULID | Primary key |
| event_id | ULID | FK to events |
| user_id | ULID | FK to users |
| rsvp_status | enum | pending, available, unavailable, tentative |
| rsvp_note | text? | Response note |
| rsvp_responded_at | timestamp? | When responded |
| invited_at | timestamp | When invited |
| reminder_sent_at | timestamp? | Last reminder |
| created_at | timestamp | Created |
| updated_at | timestamp | Updated |
music_numbers
| Column | Type | Description |
|---|---|---|
| id | ULID | Primary key |
| title | string | Song title |
| artist | string? | Original artist |
| genre | string? | Genre/style |
| duration_seconds | integer? | Duration in seconds |
| key | string? | Musical key (e.g., "Am", "G") |
| tempo_bpm | integer? | Tempo in BPM |
| time_signature | string? | Time signature (e.g., "4/4") |
| lyrics | text? | Full lyrics |
| notes | text? | Performance notes |
| tags | json? | Array of tags |
| play_count | integer | Times played (default: 0) |
| last_played_at | timestamp? | Last performed |
| is_active | boolean | Active in catalog |
| created_by | ULID? | FK to users |
| created_at | timestamp | Created |
| updated_at | timestamp | Updated |
music_attachments
| Column | Type | Description |
|---|---|---|
| id | ULID | Primary key |
| music_number_id | ULID | FK to music_numbers |
| file_name | string | Stored filename |
| original_name | string | Original filename |
| file_path | string | Storage path |
| file_type | enum | lyrics, chords, sheet_music, audio, other |
| file_size | integer | Size in bytes |
| mime_type | string | MIME type |
| created_at | timestamp | Created |
| updated_at | timestamp | Updated |
setlists
| Column | Type | Description |
|---|---|---|
| id | ULID | Primary key |
| name | string | Setlist name |
| description | text? | Description |
| total_duration_seconds | integer? | Calculated total |
| is_template | boolean | Is a template |
| is_archived | boolean | Archived |
| created_by | ULID? | FK to users |
| created_at | timestamp | Created |
| updated_at | timestamp | Updated |
setlist_items
| Column | Type | Description |
|---|---|---|
| id | ULID | Primary key |
| setlist_id | ULID | FK to setlists |
| music_number_id | ULID? | FK to music_numbers |
| position | integer | Order position |
| set_number | integer | Set number (1, 2, 3) |
| is_break | boolean | Is a break |
| break_duration_seconds | integer? | Break length |
| notes | string? | Item notes |
| created_at | timestamp | Created |
| updated_at | timestamp | Updated |
booking_requests
| Column | Type | Description |
|---|---|---|
| id | ULID | Primary key |
| customer_id | ULID | FK to customers |
| event_date | date | Requested date |
| start_time | time? | Requested start |
| end_time | time? | Requested end |
| location_name | string? | Venue name |
| location_address | string? | Venue address |
| event_type | string? | Type of event |
| expected_guests | integer? | Guest count |
| message | text? | Request message |
| status | enum | pending, reviewed, accepted, declined |
| admin_notes | text? | Admin notes |
| event_id | ULID? | FK to created event |
| reviewed_by | ULID? | FK to users |
| reviewed_at | timestamp? | When reviewed |
| created_at | timestamp | Created |
| updated_at | timestamp | Updated |
notifications
| Column | Type | Description |
|---|---|---|
| id | ULID | Primary key |
| user_id | ULID | FK to users |
| type | string | Notification type |
| title | string | Title |
| message | text | Message body |
| data | json? | Additional data |
| action_url | string? | Link URL |
| read_at | timestamp? | When read |
| created_at | timestamp | Created |
| updated_at | timestamp | Updated |
activity_logs
| Column | Type | Description |
|---|---|---|
| id | ULID | Primary key |
| user_id | ULID? | FK to users |
| loggable_type | string | Model class |
| loggable_id | ULID | Model ID |
| action | string | Action performed |
| description | text? | Description |
| changes | json? | Before/after data |
| ip_address | string? | Client IP |
| user_agent | string? | Browser info |
| created_at | timestamp | Created |
API Response Format
Success Response
{
"success": true,
"data": { ... },
"message": "Event created successfully",
"meta": {
"pagination": {
"current_page": 1,
"per_page": 15,
"total": 100,
"last_page": 7,
"from": 1,
"to": 15
}
}
}
Error Response
{
"success": false,
"message": "Validation failed",
"errors": {
"title": ["The title field is required."],
"event_date": ["The event date must be a future date."]
}
}
Single Resource
{
"success": true,
"data": {
"id": "01HQ3K5P7X...",
"title": "Summer Concert",
"event_date": "2025-07-15",
"status": "confirmed",
"location": {
"id": "01HQ3K5P7X...",
"name": "City Park Amphitheater"
},
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
}
Collection (Paginated)
{
"success": true,
"data": [
{ "id": "...", "title": "Event 1" },
{ "id": "...", "title": "Event 2" }
],
"meta": {
"pagination": {
"current_page": 1,
"per_page": 15,
"total": 45,
"last_page": 3
}
}
}
User Roles & Permissions
Roles
| Role | Description |
|---|---|
admin |
Full access to everything |
booking_agent |
Manage events, locations, customers |
music_manager |
Manage music catalog and setlists |
member |
View events, RSVP, view music |
Permissions Matrix
| Resource | Admin | Booking Agent | Music Manager | Member |
|---|---|---|---|---|
| Members | CRUD | Read | Read | Read |
| Events | CRUD | CRUD | Read | Read |
| Locations | CRUD | CRUD | Read | Read |
| Customers | CRUD | CRUD | Read | - |
| Music | CRUD | Read | CRUD | Read |
| Setlists | CRUD | Read | CRUD | Read |
| Booking Requests | CRUD | CRUD | - | - |
| RSVP | All | All | Own | Own |
File Storage
Structure
storage/app/
├── public/
│ ├── avatars/ # User avatars
│ └── music/ # Music attachments
│ ├── lyrics/
│ ├── chords/
│ ├── sheet_music/
│ └── audio/
└── private/
└── exports/ # Generated reports
File Types Allowed
| Type | Extensions | Max Size |
|---|---|---|
| Avatar | jpg, png, webp | 2 MB |
| Lyrics | txt, pdf, docx | 5 MB |
| Chords | pdf, png, jpg | 10 MB |
| Sheet Music | pdf, png, jpg | 10 MB |
| Audio | mp3, wav, m4a | 50 MB |
Architectural Decisions
ADR-001: API-First Architecture
Status: Accepted
Date: 2025-01-01
Context: We need to build a web application with three separate SPAs (Admin, Band, Customers) that may have mobile clients in the future.
Decision: Implement a completely separated frontend and backend communicating via RESTful JSON API.
Consequences:
- ✅ Frontend and backend can be developed/deployed independently
- ✅ Easy to add mobile or other clients later
- ✅ Clear API contracts
- ✅ Better scalability options
- ⚠️ More complex initial setup
- ⚠️ Requires CORS configuration
ADR-002: Laravel Sanctum for Authentication
Status: Accepted
Date: 2025-01-01
Context: Need authentication for SPAs that's secure and simple to implement.
Decision: Use Laravel Sanctum with token-based authentication for the SPAs.
Alternatives Considered:
- Passport: Too complex for our needs (OAuth2 overkill for first-party SPA)
- JWT: Requires token storage in localStorage (XSS vulnerable)
Consequences:
- ✅ Simple token-based auth for multiple SPAs
- ✅ Built into Laravel, minimal setup
- ✅ Works well with separate domains
- ⚠️ Need to handle token storage securely
ADR-003: TanStack Query for Server State
Status: Accepted
Date: 2025-01-01
Context: Need efficient data fetching with caching, background updates, and optimistic updates.
Decision: Use TanStack Query (Vue Query) for all server state management.
Consequences:
- ✅ Automatic caching and deduplication
- ✅ Built-in loading/error states
- ✅ Background refetching
- ✅ Optimistic updates for better UX
- ✅ DevTools for debugging
- ⚠️ Learning curve for query key management
ADR-004: Action Pattern for Business Logic
Status: Accepted
Date: 2025-01-01
Context: Controllers should be thin, and business logic needs to be reusable and testable.
Decision: Use single-responsibility Action classes for all business logic.
Pattern:
class CreateEventAction
{
public function execute(array $data): Event
{
// Business logic here
}
}
// Usage in controller
public function store(Request $request, CreateEventAction $action)
{
return $action->execute($request->validated());
}
Consequences:
- ✅ Single Responsibility Principle
- ✅ Easy to test in isolation
- ✅ Reusable across controllers, commands, jobs
- ⚠️ More files to manage
ADR-005: Vuexy for All SPAs
Status: Accepted
Date: 2025-01-01
Context: Need consistent UI across three SPAs with professional admin components.
Decision: Use Vuexy Vue template for all SPAs (full version for Admin, starter-kit for Band/Customers).
Consequences:
- ✅ Consistent UI/UX across all portals
- ✅ Pre-built admin components
- ✅ Single learning curve for the team
- ✅ Professional look out of the box
- ⚠️ License cost
- ⚠️ Dependency on third-party template
Security Architecture
Defense in Depth
┌─────────────────────────────────────────────────────────────┐
│ 1. Network Layer │
│ - HTTPS only │
│ - Rate limiting │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Application Layer │
│ - CORS validation │
│ - Input validation (Form Requests) │
│ - SQL injection prevention (Eloquent) │
│ - XSS prevention (Vue escaping) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Authentication & Authorization │
│ - Sanctum token authentication │
│ - Role-based access control │
│ - Resource authorization (Policies) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. Data Layer │
│ - Encrypted connections (TLS) │
│ - Sensitive data hashing (bcrypt) │
│ - Database credentials via environment │
└─────────────────────────────────────────────────────────────┘
CORS Configuration
// config/cors.php
'allowed_origins' => [
'http://localhost:5173', // Admin
'http://localhost:5174', // Band
'http://localhost:5175', // Customers
],
'supports_credentials' => true,
Performance Considerations
Backend Optimizations
-
Database
- Eager loading relationships (
with()) - Database indexes on filtered/sorted columns
- Query caching for expensive operations
- Eager loading relationships (
-
Caching Strategy
// Cache expensive queries Cache::remember('stats:dashboard', 3600, fn() => $this->calculateStats() ); -
Queue Heavy Operations
- Email sending
- File processing
- Report generation
Frontend Optimizations
-
Code Splitting
- Lazy load routes
- Dynamic imports for heavy components
-
Query Optimization
// Deduplicate requests useQuery({ queryKey: ['events'], staleTime: 5 * 60 * 1000 }) // Prefetch on hover queryClient.prefetchQuery({ queryKey: ['event', id] }) -
Bundle Size
- Tree shaking enabled
- Dynamic imports for routes
Monitoring & Observability
Logging Strategy
| Level | Use Case | Example |
|---|---|---|
| DEBUG | Development only | Query details, variable dumps |
| INFO | Normal operations | User login, API calls |
| WARNING | Unexpected but handled | Rate limit approached |
| ERROR | Errors requiring attention | Failed payment |
| CRITICAL | System failures | Database down |
Health Checks
GET /api/v1/health
{
"status": "healthy",
"checks": {
"database": "ok",
"redis": "ok",
"queue": "ok"
}
}
Model Relationships
User
- User has many EventInvitations
- User has many Notifications
- User has many ActivityLogs
- User has many created Events (as creator)
- User has many created MusicNumbers (as creator)
- User has many created Setlists (as creator)
Event
- Event belongs to Location (nullable)
- Event belongs to Customer (nullable)
- Event belongs to Setlist (nullable)
- Event belongs to User (created_by)
- Event has many EventInvitations
- Event has many Users through EventInvitations (invited members)
EventInvitation
- EventInvitation belongs to Event
- EventInvitation belongs to User
Location
- Location has many Events
Customer
- Customer belongs to User (nullable, for portal access)
- Customer has many Events
- Customer has many BookingRequests
MusicNumber
- MusicNumber belongs to User (created_by, nullable)
- MusicNumber has many MusicAttachments
- MusicNumber has many SetlistItems
- MusicNumber has many Setlists through SetlistItems
MusicAttachment
- MusicAttachment belongs to MusicNumber
Setlist
- Setlist belongs to User (created_by, nullable)
- Setlist has many SetlistItems
- Setlist has many MusicNumbers through SetlistItems
- Setlist has many Events
SetlistItem
- SetlistItem belongs to Setlist
- SetlistItem belongs to MusicNumber (nullable, null when is_break = true)
BookingRequest
- BookingRequest belongs to Customer
- BookingRequest belongs to Event (nullable, when accepted)
- BookingRequest belongs to User (reviewed_by, nullable)
Notification
- Notification belongs to User
ActivityLog
- ActivityLog belongs to User (nullable)
- ActivityLog is polymorphic (loggable_type, loggable_id)
Last updated: 2025-01-01