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​

The diagram above shows the original core scheduling/membership/payment schema. Later feature areas added their own tables on top of it, summarized below rather than drawn into the diagram.

TablePurpose
usersStudio manager and instructor accounts (login credentials)
clientsClients 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 booking 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

Other tables (added by later features)​

These were not part of the original diagram above; each belongs to a feature area shipped after the initial schema.

Feature areaTables
Appointments (1:1 bookings)appointments, appointment_services, appointment_service_locations, instructor_availability
Retail / POSproducts, product_sales, product_sale_items
Gift cardsgift_cards, gift_card_redemptions
Promo codespromo_codes, promo_code_usages
Invoicinginvoices
Marketing Automationsmarketing_automations, marketing_automation_enrollments
Tags & Smart Lists targetingtags, client_tags, auto_tag_rules
Waiverswaivers, waiver_signatures
SMS messagingsms_templates, sms_event_assignments
Staff & payrollroles, staff_pay_components, payroll_runs, payroll_line_items
Stripe billing (recurring memberships)stripe_customers, stripe_prices, stripe_checkout_sessions, stripe_subscriptions, stripe_webhook_events — alongside the memberships.stripe_subscription_id column shown above
Push/in-app notificationsnotification_log
Migration (see Migration overview)migration_job

ai_usage_log and ai_response_cache also exist (AI Assistant request logging and response caching) but are intentionally left out of both tables above — they're operational/internal rather than studio business data, and their exact shape is more likely to change as the AI Assistant evolves.

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