Skip to main content

Database Schema

Agon uses SQLite (WAL mode) as its local database. All datetime values are stored in UTC (timezone-naive).

Entity Relationship Diagram

erDiagram
users {
int id PK
string email UK
string password_hash
string full_name
string role
bool is_active
datetime created_at
datetime updated_at
}

clients {
int id PK
int location_id
string email UK
string password_hash
string full_name
string phone
date dob
string notes
string push_token
bool is_active
datetime created_at
datetime updated_at
}

instructors {
int id PK
int user_id FK
int location_id
string bio
string specialties
datetime created_at
datetime updated_at
}

class_templates {
int id PK
int location_id
string name
string description
int duration_minutes
int default_capacity
string color
int default_instructor_id FK
bool is_active
datetime created_at
datetime updated_at
}

scheduled_classes {
int id PK
int template_id FK
int instructor_id FK
int location_id
datetime starts_at
datetime ends_at
int capacity
string status
string meeting_url
datetime created_at
datetime updated_at
}

bookings {
int id PK
int client_id FK
int scheduled_class_id FK
int location_id
string status
datetime created_at
datetime updated_at
}

checkins {
int id PK
int booking_id FK
int client_id FK
int scheduled_class_id FK
int checked_in_by FK
string method
datetime checked_in_at
}

waitlist {
int id PK
int client_id FK
int scheduled_class_id FK
int position
string status
datetime expires_at
datetime created_at
datetime updated_at
}

membership_types {
int id PK
int location_id
string name
string type
float price
string currency
int credits
int duration_days
bool is_active
datetime created_at
datetime updated_at
}

memberships {
int id PK
int client_id FK
int membership_type_id FK
int location_id
string status
date starts_at
date expires_at
int credits_remaining
int credits_used
string stripe_subscription_id
datetime created_at
datetime updated_at
}

payments {
int id PK
int client_id FK
int membership_id FK
float amount
string currency
string status
string provider
string provider_payment_id
datetime paid_at
datetime created_at
datetime updated_at
}

locations {
int id PK
string name
string address
string phone
bool is_active
datetime created_at
datetime updated_at
}

studio_settings {
int id PK
string studio_name
string timezone
string currency
string stripe_public_key
string stripe_secret_key
string cloudflare_tunnel_token
int booking_cancellation_hours
bool waitlist_enabled
datetime created_at
datetime updated_at
}

consent_log {
int id PK
int client_id FK
string type
bool granted
string ip_address
datetime created_at
}

email_templates {
int id PK
string name
string subject
string body_html
string variables
bool is_active
datetime created_at
datetime updated_at
}

email_event_assignments {
int id PK
string event_type
int template_id FK
}

smart_lists {
int id PK
string name
string filters_json
datetime created_at
datetime updated_at
}

users ||--o{ instructors : "has profile"
clients ||--o{ bookings : "makes"
clients ||--o{ memberships : "holds"
clients ||--o{ waitlist : "joins"
clients ||--o{ payments : "makes"
clients ||--o{ consent_log : "logs"
scheduled_classes ||--o{ bookings : "receives"
scheduled_classes ||--o{ waitlist : "has"
scheduled_classes ||--o{ checkins : "records"
class_templates ||--o{ scheduled_classes : "spawns"
instructors ||--o{ scheduled_classes : "teaches"
instructors |o--o{ class_templates : "default for"
bookings ||--o| checkins : "has"
memberships ||--o{ payments : "paid via"
membership_types ||--o{ memberships : "defines"
email_templates |o--o{ email_event_assignments : "assigned to"

Key Tables

TablePurpose
usersStudio manager and instructor accounts (login credentials)
clientsGym members who book classes via the mobile app
instructorsOne-to-one profile linked to a users row
class_templatesReusable class definition (Yoga, HIIT, Pilates…)
scheduled_classesA specific occurrence of a template at a date/time
bookingsA client's reservation for a scheduled class
checkinsConfirmed attendance record for a booking
waitlistQueue entry when a class is full
membershipsA client's active subscription or credit pack
membership_typesTemplate for recurring plans or credit packs
paymentsPayment records (Stripe or manual)
locationsPhysical studio location (V1: always id=1)
studio_settingsSingleton row with studio configuration
consent_logGDPR consent events
email_templatesCustom transactional email bodies
email_event_assignmentsMaps lifecycle events to custom templates
smart_listsSaved client filter queries for targeted messaging

Design Notes

UTC-naive datetimes

All DateTime columns store UTC values without timezone info. The application layer always compares with utcnow() from app.utils. Never insert timezone-aware datetimes into the database.

location_id convention

Every business entity carries location_id (default 1). Location 1 (Main Studio) is seeded at migration time. This column is the hook for V2 multi-location support — no schema change needed.

Soft delete vs hard delete

  • Soft delete (status/is_active): clients, instructors, class templates, membership types, bookings, memberships, scheduled classes.
  • Hard delete: Only on explicit GDPR erasure request. Personal data is anonymised in-place; booking/payment history rows are retained for audit purposes.

See ARCHITECTURE.md in the codebase for the full delete strategy and cascade rules.

Composite indexes

Performance-critical query patterns are covered by composite indexes:

IndexColumnsQuery pattern
idx_booking_class_statusscheduled_class_id, statusAll confirmed bookings for a class
idx_booking_client_statusclient_id, statusClient's booking history
idx_membership_client_statusclient_id, statusClient's active membership lookup
idx_class_starts_at_location_statusstarts_at, location_id, statusCalendar date-range queries