Skip to content

Commit d7f06b3

Browse files
committed
Merge remote-tracking branch 'origin/dev' into feature/7-roles
2 parents 971fb30 + b606166 commit d7f06b3

9 files changed

Lines changed: 561 additions & 201 deletions

File tree

docs/coaching.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Coaching Sessions Table
2+
3+
One-on-one sessions booked between a user and a coach. Linked to a `coaching_session`-type entry in `services`.
4+
5+
```mermaid
6+
erDiagram
7+
coaching_sessions {
8+
uuid id PK
9+
uuid service_id FK
10+
uuid coach_id FK
11+
uuid user_id FK
12+
timestamp scheduled_at "null until a slot is confirmed"
13+
int duration_minutes
14+
session_status status "pending | confirmed | cancelled | completed"
15+
text meeting_url
16+
text notes
17+
jsonb selected_time_slots "array of {start, end} objects"
18+
timestamp created_at
19+
timestamp updated_at
20+
}
21+
22+
profiles ||--o{ coaching_sessions : "coach leads"
23+
profiles ||--o{ coaching_sessions : "user attends"
24+
services ||--o{ coaching_sessions : "fulfilled by"
25+
```
26+
27+
## Notes
28+
29+
- `scheduled_at` is null by default — it is set once the user selects a specific slot from `selected_time_slots`.
30+
- `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" }]`.
31+
- `meeting_url` is provided by the coach after confirmation.
32+
- `status = completed` is set after the session ends.

docs/profiles.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Profiles Table
2+
3+
Mirrors Supabase `auth.users` — populated via a database trigger on signup. Stores display data and the user's role within the platform.
4+
5+
```mermaid
6+
erDiagram
7+
profiles {
8+
uuid id PK "references auth.users(id)"
9+
text first_name
10+
text last_name
11+
role role "user | admin | coach"
12+
timestamp created_at
13+
timestamp updated_at
14+
}
15+
```
16+
17+
## Notes
18+
19+
- `id` is **not** auto-generated — it is set to the corresponding `auth.users.id` from Supabase Auth.
20+
- `role` controls access: `user` is a regular member, `coach` can manage and lead sessions, `admin` has full access.

docs/schema-overview.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# MCLD Platform — Full Schema Overview
2+
3+
```mermaid
4+
erDiagram
5+
profiles {
6+
uuid id PK
7+
text first_name
8+
text last_name
9+
role role
10+
timestamp created_at
11+
timestamp updated_at
12+
}
13+
14+
services {
15+
uuid id PK
16+
text title
17+
text description
18+
service_type type
19+
jsonb scheduled_at "null for coaching_session"
20+
int duration_minutes
21+
int price
22+
boolean is_active
23+
timestamp created_at
24+
timestamp updated_at
25+
}
26+
27+
service_bookings {
28+
uuid id PK
29+
uuid user_id FK
30+
uuid service_id FK
31+
booking_status status
32+
text notes
33+
boolean is_active
34+
timestamp created_at
35+
timestamp updated_at
36+
}
37+
38+
webinars {
39+
uuid id PK
40+
text title
41+
text description
42+
webinar_tier tier
43+
int duration_minutes
44+
text youtube_url
45+
boolean is_active
46+
timestamp created_at
47+
timestamp updated_at
48+
}
49+
50+
coaching_sessions {
51+
uuid id PK
52+
uuid service_id FK
53+
uuid coach_id FK
54+
uuid user_id FK
55+
timestamp scheduled_at
56+
int duration_minutes
57+
session_status status
58+
text meeting_url
59+
text notes
60+
jsonb selected_time_slots "array of {start, end} objects"
61+
timestamp created_at
62+
timestamp updated_at
63+
}
64+
65+
profiles ||--o{ service_bookings : "books"
66+
services ||--o{ service_bookings : "booked via"
67+
profiles ||--o{ coaching_sessions : "coaches"
68+
profiles ||--o{ coaching_sessions : "attends"
69+
services ||--o{ coaching_sessions : "fulfilled by"
70+
```
71+
72+
## Enums
73+
74+
| Enum | Values |
75+
|---|---|
76+
| `role` | `user`, `admin`, `coach` |
77+
| `service_type` | `coaching_session`, `booking` |
78+
| `booking_status` | `pending`, `confirmed`, `cancelled` |
79+
| `webinar_tier` | `free`, `premium` |
80+
| `session_status` | `pending`, `confirmed`, `cancelled`, `completed` |

docs/services.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Services & Service Bookings Tables
2+
3+
## Services
4+
5+
The central catalog of offerings on the platform. Two types:
6+
- **`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.
7+
- **`coaching_session`** — a coaching offering where the user proposes availability. `scheduled_at` is null — timing is handled through the `coaching_sessions` table.
8+
9+
```mermaid
10+
erDiagram
11+
services {
12+
uuid id PK
13+
text title
14+
text description
15+
service_type type "coaching_session | booking"
16+
jsonb scheduled_at "array of {start, end} objects — null for coaching_session"
17+
int duration_minutes
18+
int price "in cents"
19+
boolean is_active
20+
timestamp created_at
21+
timestamp updated_at
22+
}
23+
```
24+
25+
## Service Bookings
26+
27+
A user's purchase of a booking-type service.
28+
29+
```mermaid
30+
erDiagram
31+
service_bookings {
32+
uuid id PK
33+
uuid user_id FK
34+
uuid service_id FK
35+
booking_status status "pending | confirmed | cancelled"
36+
text notes
37+
boolean is_active
38+
timestamp created_at
39+
timestamp updated_at
40+
}
41+
42+
services ||--o{ service_bookings : "booked via"
43+
```
44+
45+
## Notes
46+
47+
- `price` is stored in **cents** (integer) to avoid floating-point issues.
48+
- `is_active = false` hides a service without deleting historical bookings.
49+
- `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" }]`.

docs/webinars.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Webinars Table
2+
3+
Online video content hosted on the platform. Two tiers:
4+
- **`free`** — accessible to all users.
5+
- **`premium`** — accessible to active subscribers only.
6+
7+
```mermaid
8+
erDiagram
9+
webinars {
10+
uuid id PK
11+
text title
12+
text description
13+
webinar_tier tier "free | premium"
14+
int duration_minutes
15+
text youtube_url
16+
boolean is_active
17+
timestamp created_at
18+
timestamp updated_at
19+
}
20+
```
21+
22+
## Notes
23+
24+
- Access is determined by the user's subscription status, not registration — there is no sign-up flow.
25+
- `youtube_url` is the link to the YouTube video.
26+
- `is_active = false` hides a webinar without deleting it.

drizzle.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { defineConfig } from "drizzle-kit";
44
export default defineConfig({
55
out: "./drizzle",
66
schema: "./lib/db/schema.ts",
7+
schemaFilter: ["public"],
78
dialect: "postgresql",
89
dbCredentials: {
910
url: process.env.DATABASE_URL!,

lib/db/schema.ts

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,129 @@
1-
import { pgEnum, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
1+
import {
2+
pgTable,
3+
text,
4+
timestamp,
5+
uuid,
6+
pgEnum,
7+
integer,
8+
boolean,
9+
jsonb,
10+
} from "drizzle-orm/pg-core";
211

3-
export const userRoleEnum = pgEnum("user_role", ["admin", "coach", "user"]);
12+
export const roleEnum = pgEnum("role", ["user", "admin", "coach"]);
13+
export const serviceTypeEnum = pgEnum("service_type", [
14+
"coaching_session",
15+
"booking",
16+
]);
17+
export const bookingStatusEnum = pgEnum("booking_status", [
18+
"pending",
19+
"confirmed",
20+
"cancelled",
21+
]);
22+
export const webinarTierEnum = pgEnum("webinar_tier", ["free", "premium"]);
23+
export const sessionStatusEnum = pgEnum("session_status", [
24+
"pending",
25+
"confirmed",
26+
"cancelled",
27+
"completed",
28+
]);
429

530
export const profiles = pgTable("profiles", {
631
id: uuid("id").primaryKey(),
7-
firstName: text("first_name"),
8-
lastName: text("last_name"),
9-
stripeCustomerId: text("stripe_customer_id"),
10-
role: userRoleEnum("role").default("user").notNull(),
11-
createdAt: timestamp("created_at").defaultNow(),
12-
updatedAt: timestamp("updated_at"),
32+
firstName: text("first_name").notNull(),
33+
lastName: text("last_name").notNull(),
34+
role: roleEnum("role").notNull().default("user"),
35+
stripeCustomerId: text("stripe_customer_id").unique(),
36+
createdAt: timestamp("created_at").defaultNow().notNull(),
37+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
38+
});
39+
40+
export const services = pgTable("services", {
41+
id: uuid("id").primaryKey().defaultRandom(),
42+
title: text("title").notNull(),
43+
description: text("description"),
44+
type: serviceTypeEnum("type").notNull(),
45+
scheduledAt: jsonb("scheduled_at"),
46+
durationMinutes: integer("duration_minutes").notNull(),
47+
price: integer("price").notNull().default(0),
48+
isActive: boolean("is_active").notNull().default(true),
49+
createdAt: timestamp("created_at").defaultNow().notNull(),
50+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
51+
});
52+
53+
export const serviceBookings = pgTable("service_bookings", {
54+
id: uuid("id").primaryKey().defaultRandom(),
55+
userId: uuid("user_id")
56+
.references(() => profiles.id, { onDelete: "cascade" })
57+
.notNull(),
58+
serviceId: uuid("service_id")
59+
.references(() => services.id, { onDelete: "cascade" })
60+
.notNull(),
61+
status: bookingStatusEnum("status").notNull().default("pending"),
62+
notes: text("notes"),
63+
isActive: boolean("is_active").notNull().default(true),
64+
createdAt: timestamp("created_at").defaultNow().notNull(),
65+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
66+
});
67+
68+
export const webinars = pgTable("webinars", {
69+
id: uuid("id").primaryKey().defaultRandom(),
70+
title: text("title").notNull(),
71+
description: text("description"),
72+
tier: webinarTierEnum("tier").notNull().default("free"),
73+
durationMinutes: integer("duration_minutes").notNull(),
74+
youtubeUrl: text("youtube_url"),
75+
isActive: boolean("is_active").notNull().default(true),
76+
createdAt: timestamp("created_at").defaultNow().notNull(),
77+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
78+
});
79+
80+
export const coachingSessions = pgTable("coaching_sessions", {
81+
id: uuid("id").primaryKey().defaultRandom(),
82+
serviceId: uuid("service_id")
83+
.references(() => services.id, { onDelete: "cascade" })
84+
.notNull(),
85+
coachId: uuid("coach_id")
86+
.references(() => profiles.id, { onDelete: "cascade" })
87+
.notNull(),
88+
userId: uuid("user_id")
89+
.references(() => profiles.id, { onDelete: "cascade" })
90+
.notNull(),
91+
scheduledAt: timestamp("scheduled_at"),
92+
durationMinutes: integer("duration_minutes").notNull().default(60),
93+
status: sessionStatusEnum("status").notNull().default("pending"),
94+
meetingUrl: text("meeting_url"),
95+
notes: text("notes"),
96+
selectedTimeSlots: jsonb("selected_time_slots").notNull(),
97+
createdAt: timestamp("created_at").defaultNow().notNull(),
98+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
99+
});
100+
101+
export const subscriptions = pgTable("subscriptions", {
102+
id: uuid("id").primaryKey().defaultRandom(),
103+
userId: uuid("user_id")
104+
.references(() => profiles.id, { onDelete: "cascade" })
105+
.notNull()
106+
.unique(),
107+
stripeSubscriptionId: text("stripe_subscription_id").unique(),
108+
status: text("status").notNull().default("none"),
109+
stripePriceId: text("stripe_price_id"),
110+
cancelAtPeriodEnd: boolean("cancel_at_period_end").notNull().default(false),
111+
paymentMethodBrand: text("payment_method_brand"),
112+
paymentMethodLast4: text("payment_method_last4"),
113+
createdAt: timestamp("created_at").defaultNow().notNull(),
114+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
115+
});
116+
117+
export const purchases = pgTable("purchases", {
118+
id: uuid("id").primaryKey().defaultRandom(),
119+
userId: uuid("user_id")
120+
.references(() => profiles.id, { onDelete: "cascade" })
121+
.notNull(),
122+
stripePriceId: text("stripe_price_id").notNull(),
123+
stripeSessionId: text("stripe_session_id").notNull().unique(),
124+
productName: text("product_name").notNull(),
125+
amount: integer("amount").notNull(),
126+
currency: text("currency").notNull(),
127+
createdAt: timestamp("created_at").defaultNow().notNull(),
128+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
13129
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"class-variance-authority": "^0.7.1",
2121
"clsx": "^2.1.1",
2222
"dotenv": "^17.3.1",
23-
"drizzle-orm": "^0.45.1",
23+
"drizzle-orm": "^0.45.2",
2424
"lucide-react": "^0.577.0",
2525
"next": "16.2.1",
2626
"postgres": "^3.4.8",

0 commit comments

Comments
 (0)