Scheduling Tables
The scheduling system supports dual-mode booking: client-owned booking links (for candidate scheduling) and user-owned booking links (for internal sales meetings). Both modes share the same booking_links table but use separate calendar connection and meeting tables.
booking_links
Public booking URLs. Each link generates a page at /book/:slug where someone can self-schedule a meeting.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | uuid | NO | gen_random_uuid() | Primary key |
slug | text | NO | URL slug (unique) -- forms the booking URL /book/:slug | |
title | text | YES | Displayed on the booking page | |
duration_minutes | integer | YES | 30 | Meeting duration |
client_id | uuid | YES | FK to clients.id (nullable) | |
user_id | uuid | YES | FK to auth.users.id (nullable) | |
market_id | uuid | YES | FK to markets.id (optional) | |
campaign_id | uuid | YES | FK to campaigns.id (optional) | |
is_active | boolean | YES | true | Whether the link is accepting bookings |
custom_questions | jsonb | YES | Additional form questions on booking page | |
created_at | timestamptz | YES | now() | |
updated_at | timestamptz | YES | now() |
Ownership Constraint
CHECK (client_id IS NOT NULL OR user_id IS NOT NULL)
A booking link must be owned by either a client or a user (or both, though in practice it is one or the other).
Dual-Mode Detection
The booking page determines the mode based on the ownership:
const isInternalLink = !bookingLink?.client_id && !!bookingLink?.user_id;
| Mode | Ownership | Calendar Table | Meeting Table | Contact Table |
|---|---|---|---|---|
| Client | client_id set | calendar_connections | meetings | contacts |
| Internal (User) | user_id set, client_id null | user_calendar_connections | sales_meetings | sales_prospects |
calendar_connections
OAuth tokens for client-owned calendar integrations. Used to check availability and create events in the client's calendar.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | uuid | NO | gen_random_uuid() | Primary key |
client_id | uuid | NO | FK to clients.id | |
provider | text | NO | Calendar provider: google or outlook | |
access_token | text | YES | OAuth access token (encrypted at rest) | |
refresh_token | text | YES | OAuth refresh token | |
token_expires_at | timestamptz | YES | Token expiration time | |
calendar_id | text | YES | Selected calendar ID (e.g., primary) | |
connected_email | text | YES | Email associated with the calendar | |
is_active | boolean | YES | true | Whether connection is active |
created_at | timestamptz | YES | now() | |
updated_at | timestamptz | YES | now() |
user_calendar_connections
OAuth tokens for internal user-owned calendar integrations. Same schema as calendar_connections but keyed by user_id instead of client_id.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | uuid | NO | gen_random_uuid() | Primary key |
user_id | uuid | NO | FK to auth.users.id | |
provider | text | NO | Calendar provider: google or outlook | |
access_token | text | YES | OAuth access token | |
refresh_token | text | YES | OAuth refresh token | |
token_expires_at | timestamptz | YES | Token expiration time | |
calendar_id | text | YES | Selected calendar ID | |
connected_email | text | YES | Email associated with the calendar | |
is_active | boolean | YES | true | Whether connection is active |
created_at | timestamptz | YES | now() | |
updated_at | timestamptz | YES | now() |
Unique constraint: (user_id, provider) -- a user can only have one connection per provider.
OAuth State Format
The OAuth callback functions distinguish between client and user mode using the state parameter:
| Mode | State Format | Example |
|---|---|---|
| Client | {clientId}:{provider} | abc123:google |
| Internal (User) | user:{userId}:{provider} | user:def456:google |
The callback function parses the state to determine which table to upsert into:
const parts = state.split(':');
if (parts[0] === 'user') {
// Internal mode: upsert into user_calendar_connections
const userId = parts[1];
const provider = parts[2];
} else {
// Client mode: upsert into calendar_connections
const clientId = parts[0];
const provider = parts[1];
}