Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions docs/coaching.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,42 @@
# Coaching Sessions Table

One-on-one sessions booked between a user and a coach. Linked to a `coaching_session`-type entry in `services`.
One-on-one sessions booked between a user and a coordinator. Each session is linked
to a `private_lessons`-type entry in `services`.

```mermaid
erDiagram
coaching_sessions {
uuid id PK
uuid service_id FK
uuid coach_id FK
uuid coordinator_id FK
uuid user_id FK
uuid child_id FK "nullable; null means adult registration"
timestamp scheduled_at "null until a slot is confirmed"
int duration_minutes
session_status status "pending | confirmed | cancelled | completed"
session_status status "awaiting_payment | pending | confirmed | cancelled | completed"
text meeting_url
text notes
jsonb selected_time_slots "array of {start, end} objects"
text stripe_order_id "unique"
timestamp created_at
timestamp updated_at
}

profiles ||--o{ coaching_sessions : "coach leads"
profiles ||--o{ coaching_sessions : "coordinator leads"
profiles ||--o{ coaching_sessions : "user attends"
services ||--o{ coaching_sessions : "fulfilled by"
children |o--o{ coaching_sessions : "registered for"
```

## Notes

- `scheduled_at` is null by default — it is set once the user selects a specific slot from `selected_time_slots`.
- `selected_time_slots` is a JSON array of `{ start, end }` objects (ISO 8601 strings) representing the time options offered to the user e.g. `[{ "start": "2026-04-14T14:00:00Z", "end": "2026-04-14T17:00:00Z" }]`.
- `meeting_url` is provided by the coach after confirmation.
- `coordinator_id` references the `profiles` row of the coordinator leading the session;
`user_id` is the attending user. `child_id` is set when the session is booked on behalf
of a child (null for adult registrations).
- `scheduled_at` is null by default — it is set once a specific slot is confirmed from
`selected_time_slots`.
- `selected_time_slots` is a **required** JSON array of `{ start, end }` objects (ISO 8601
strings) representing the availability windows the user offered when requesting the
session, e.g. `[{ "start": "2026-04-14T14:00:00Z", "end": "2026-04-14T17:00:00Z" }]`.
- `meeting_url` is provided after the session is confirmed.
- `stripe_order_id` links the session to its Stripe payment (unique).
- `status = completed` is set after the session ends.
16 changes: 11 additions & 5 deletions docs/profiles.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
# Profiles Table

Mirrors Supabase `auth.users` — populated via a database trigger on signup. Stores display data and the user's role within the platform. The last_login_at property is updated via a Server Action every time a user logs in.
Mirrors Supabase `auth.users` — populated via a database trigger on signup. Stores display data, contact details, and the user's role within the platform. `last_login_at` is updated via a Server Action every time a user logs in.

```mermaid
erDiagram
profiles {
uuid id PK "references auth.users(id)"
text first_name
text last_name
text stripe_customer_id "unique"
role role "user | admin | coach"
role role "user | admin | coordinator"
text address "nullable"
gender gender "nullable; male | female | prefer_not_to_say"
date dob "nullable"
text phone "nullable"
text stripe_customer_id "unique, nullable"
timestamp last_login_at
timestamp created_at
timestamp updated_at
timestamp last_login_at
}
```

## Notes

- `id` is **not** auto-generated — it is set to the corresponding `auth.users.id` from Supabase Auth.
- `role` controls access: `user` is a regular member, `coach` can manage and lead sessions, `admin` has full access.
- `role` controls access (defaults to `user`): `user` is a regular member, `coordinator` can manage and lead services/coaching sessions, `admin` has full access. Values are also defined in `lib/roles.ts`.
- `first_name` and `last_name` are required; `address`, `gender`, `dob`, and `phone` are optional profile details.
- `stripe_customer_id` links the profile to its Stripe customer once created; it is unique.
102 changes: 77 additions & 25 deletions docs/schema-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
```mermaid
erDiagram
profiles {
uuid id PK
uuid id PK "references auth.users(id)"
text first_name
text last_name
role role
text stripe_customer_id
text address
gender gender
date dob
text phone
text stripe_customer_id "unique"
timestamp last_login_at
timestamp created_at
timestamp updated_at
Expand All @@ -25,13 +29,14 @@ erDiagram
service_type type
date start_date "null for private_lessons"
date end_date "null for private_lessons"
jsonb slots "array of {dayOfWeek, time}; null for private_lessons"
jsonb slots "ProgramSlot[] = {dayOfWeek, time}; null for private_lessons"
int duration_minutes
text stripe_product_id
service_status status
uuid coach_id FK "null for programs"
uuid form_id FK "nullable"
uuid coordinator_id FK "required for private_lessons (see constraint)"
uuid form_id FK "nullable; set null on form delete"
boolean is_for_children
boolean requires_subscription
timestamp created_at
timestamp updated_at
}
Expand All @@ -44,7 +49,7 @@ erDiagram
booking_status status
text notes
boolean is_active
text stripe_order_id
text stripe_order_id "unique"
timestamp created_at
timestamp updated_at
}
Expand All @@ -64,26 +69,23 @@ erDiagram
coaching_sessions {
uuid id PK
uuid service_id FK
uuid coach_id FK
uuid coordinator_id FK
uuid user_id FK
uuid child_id FK "nullable; null means adult registration"
timestamp scheduled_at "set when slot is confirmed"
timestamp scheduled_at "set when a slot is confirmed"
session_status status
text meeting_url
text notes
jsonb selected_time_slots "array of {start, end} objects"
jsonb coach_time_slots
text coach_token
text client_token
text stripe_order_id
jsonb selected_time_slots "array of {start, end} ISO 8601 objects"
text stripe_order_id "unique"
timestamp created_at
timestamp updated_at
}

subscriptions {
uuid id PK
uuid user_id FK
text stripe_subscription_id
uuid user_id FK "unique"
text stripe_subscription_id "unique"
text status
text stripe_price_id
boolean cancel_at_period_end
Expand All @@ -97,7 +99,7 @@ erDiagram
uuid id PK
uuid user_id FK
text stripe_price_id
text stripe_session_id
text stripe_session_id "unique"
text product_name
int amount
text currency
Expand Down Expand Up @@ -135,7 +137,7 @@ erDiagram
uuid form_id FK
form_question_type type
text prompt
jsonb options
jsonb options "FormQuestionOption[] = {id, title, description?}"
int sort_order
timestamp created_at
timestamp updated_at
Expand All @@ -153,36 +155,86 @@ erDiagram
profiles ||--o{ service_bookings : "books"
services ||--o{ service_bookings : "booked via"
children |o--o{ service_bookings : "registered for"
profiles ||--o{ coaching_sessions : "coaches"
profiles ||--o{ coaching_sessions : "coordinates"
profiles ||--o{ coaching_sessions : "attends"
services ||--o{ coaching_sessions : "fulfilled by"
children |o--o{ coaching_sessions : "registered for"
profiles ||--o{ children : "parent of"
children ||--o{ emergency_contacts : "has"
profiles ||--o| subscriptions : "has"
profiles ||--o{ purchases : "makes"
profiles |o--o{ services : "coaches"
profiles |o--o{ services : "coordinates"
forms ||--o{ form_questions : "contains"
services }o--o| forms : "uses"
form_questions ||--o{ form_question_answers : "answered via"
children ||--o{ form_question_answers : "submits"
```

## Indexes
## Per-table docs

| Table | Index | Type | Condition |
|---|---|---|---|
| `service_bookings` | `service_bookings_service_id_child_id_idx` | Unique (partial) | `WHERE child_id IS NOT NULL` — prevents the same child from registering for the same program twice |
| Table(s) | Doc |
|---|---|
| `profiles` | [profiles.md](./profiles.md) |
| `services`, `service_bookings` | [services.md](./services.md) |
| `coaching_sessions` | [coaching.md](./coaching.md) |
| `webinars` | [webinars.md](./webinars.md) |

Tables without a dedicated doc (`forms`, `form_questions`, `form_question_answers`,
`children`, `emergency_contacts`, `subscriptions`, `purchases`) are covered by the
ER diagram above.

## Enums

| Enum | Values |
|---|---|
| `role` | `user`, `admin`, `coach` |
| `role` | `user`, `admin`, `coordinator` |
| `service_type` | `private_lessons`, `programs` |
| `service_status` | `active`, `disabled`, `archived`, `deleted` |
| `service_status` | `active`, `archived`, `deleted`, `disabled` |
| `booking_status` | `awaiting_payment`, `pending`, `confirmed`, `cancelled` |
| `session_status` | `awaiting_payment`, `pending`, `confirmed`, `cancelled`, `completed` |
| `webinar_tier` | `free`, `premium` |
| `gender` | `male`, `female`, `prefer_not_to_say` |
| `form_question_type` | `text`, `multiple_choices`, `checkboxes`, `user_agreement` |

## Constraints

| Table | Constraint | Rule |
|---|---|---|
| `services` | `services_private_lessons_require_coordinator` (CHECK) | A `private_lessons` service must have a `coordinator_id`. Other types may leave it null. |

## Indexes

| Table | Index | Type | Condition |
|---|---|---|---|
| `service_bookings` | `service_bookings_service_id_child_id_idx` | Unique (partial) | `WHERE child_id IS NOT NULL` — prevents the same child from registering for the same service twice |

## JSONB shapes

Some `jsonb` columns store typed structures defined in `lib/db/schema.ts`:

| Column | Shape | Notes |
|---|---|---|
| `services.slots` | `ProgramSlot[]` — `{ dayOfWeek: number; time: string }` | Recurring weekly slots for `programs`; null for `private_lessons`. |
| `coaching_sessions.selected_time_slots` | `{ start: string; end: string }[]` | ISO 8601 windows the user offered when requesting a session. Not `$type`-annotated in the schema. |
| `form_questions.options` | `FormQuestionOption[]` — `{ id: string; title: string; description?: string }` | Choices for `multiple_choices` / `checkboxes` questions; null for other types. |

## Working with the schema

- **`lib/db/schema.ts` is the single source of truth.** The Drizzle config
(`drizzle.config.ts`) points at it, and all app code imports from it. This doc,
the ER diagram, and the migration files are all derived from it.
- **Naming convention:** columns are `snake_case` in Postgres and `camelCase` in
the Drizzle/TypeScript layer (e.g. `coordinator_id` ↔ `coordinatorId`). Keep both
in sync when adding columns.
- **Changing the schema — two workflows:**
- `pnpm db:push` — applies `schema.ts` directly to the database. Fast, good for
local prototyping; does **not** create a migration file.
- `pnpm db:generate` then `pnpm db:migrate` — generates a versioned SQL migration
under `drizzle/` and applies it. Use this for changes that ship to shared/prod
environments.
- Pick one workflow per change; don't run `db:push` and `db:migrate` against the
same environment expecting them to reconcile.
- `pnpm db:studio` opens Drizzle Studio to inspect data.
- **Keep the docs in sync:** the ER diagram and enum/constraint tables above are
hand-maintained. Update them (and the relevant per-table doc) in the same PR as
any `schema.ts` change.
55 changes: 41 additions & 14 deletions docs/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,75 @@

## Services

The central catalog of offerings on the platform. Two types:
- **`booking`** — a fixed event with predetermined time slots set by the admin. Users buy it like a product with no input on timing. `scheduled_at` holds the time slots as a JSON array.
- **`coaching_session`** — a coaching offering where the user proposes availability. `scheduled_at` is null — timing is handled through the `coaching_sessions` table.
The central catalog of offerings on the platform. Two types (`service_type`):

- **`private_lessons`** — one-on-one coaching led by a coordinator. The user proposes availability and scheduling is handled through the `coaching_sessions` table. A coordinator is **required** (enforced by a check constraint).
- **`programs`** — recurring group offerings with a fixed schedule. `slots` holds the recurring weekly times, and `start_date`/`end_date` bound the program.

```mermaid
erDiagram
services {
uuid id PK
text title
text description
service_type type "coaching_session | booking"
jsonb scheduled_at "array of {start, end} objects — null for coaching_session"
service_type type "private_lessons | programs"
date start_date "null for private_lessons"
date end_date "null for private_lessons"
jsonb slots "ProgramSlot[] = {dayOfWeek, time}; null for private_lessons"
int duration_minutes
int price "in cents"
boolean is_active
text stripe_product_id
service_status status "active | archived | deleted | disabled"
uuid coordinator_id FK "required for private_lessons"
uuid form_id FK "nullable"
boolean is_for_children
boolean requires_subscription
timestamp created_at
timestamp updated_at
}

profiles |o--o{ services : "coordinates"
services }o--o| forms : "uses"
```

## Service Bookings

A user's purchase of a booking-type service.
A user's enrollment in a service. For `programs` this represents a seat; for
`private_lessons` the scheduling detail lives in `coaching_sessions`.

```mermaid
erDiagram
service_bookings {
uuid id PK
uuid user_id FK
uuid service_id FK
booking_status status "pending | confirmed | cancelled"
uuid child_id FK "nullable; null means adult registration"
booking_status status "awaiting_payment | pending | confirmed | cancelled"
text notes
boolean is_active
text stripe_order_id "unique"
timestamp created_at
timestamp updated_at
}

profiles ||--o{ service_bookings : "books"
services ||--o{ service_bookings : "booked via"
children |o--o{ service_bookings : "registered for"
```

## Notes

- `price` is stored in **cents** (integer) to avoid floating-point issues.
- `is_active = false` hides a service without deleting historical bookings.
- `scheduled_at` is a JSON array of `{ start, end }` ISO 8601 objects e.g. `[{ "start": "2026-04-15T14:00:00Z", "end": "2026-04-15T16:00:00Z" }]`.
- **`coordinator_id` is required for `private_lessons`** — enforced by the
`services_private_lessons_require_coordinator` check constraint. It may be null
for `programs`. Deleting a referenced coordinator is restricted (`onDelete: restrict`).
- `slots` is a JSON array of `ProgramSlot` objects (`{ dayOfWeek: number; time: string }`)
describing the recurring weekly schedule of a program. It is null for `private_lessons`.
- `stripe_product_id` links the service to its Stripe product (required). Pricing lives
in Stripe, not in this table.
- `status` controls visibility/lifecycle: `active` is live; `disabled` hides it
temporarily; `archived`/`deleted` retire it without dropping historical bookings.
- `is_for_children` marks services booked on behalf of a child (via `child_id` on the booking).
- `requires_subscription` (default `true`) gates the service behind an active subscription.
- `form_id` optionally attaches an intake form (see `forms` / `form_questions`); it is
set to null if the form is deleted.
- **Bookings:** `child_id` is null for adult registrations. A partial unique index
(`service_bookings_service_id_child_id_idx`) prevents the same child from being
registered for the same service twice. `stripe_order_id` links the booking to its
Stripe payment. `is_active = false` hides a booking without deleting history.
Loading