Core Tables
These are the foundational tables that power the Client Portal platform: clients, users, campaigns, contacts, meetings, calls, and notification preferences.
clients
Client organizations using the platform. Each client represents a legal search firm.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | uuid | NO | gen_random_uuid() | Primary key |
name | text | NO | Organization name | |
email | text | YES | Primary contact email | |
phone | text | YES | Primary contact phone | |
website | text | YES | Company website URL | |
status | text | YES | 'active' | Account status (active, inactive, onboarding) |
availability_settings | jsonb | YES | Weekly availability for booking (used by client-mode scheduling) | |
timezone | text | YES | 'America/New_York' | Client's timezone |
created_at | timestamptz | YES | now() | Record creation timestamp |
updated_at | timestamptz | YES | now() | Last update timestamp |
The availability_settings JSONB structure:
{
"monday": { "enabled": true, "start": "09:00", "end": "17:00" },
"tuesday": { "enabled": true, "start": "09:00", "end": "17:00" },
"wednesday": { "enabled": true, "start": "09:00", "end": "17:00" },
"thursday": { "enabled": true, "start": "09:00", "end": "17:00" },
"friday": { "enabled": true, "start": "09:00", "end": "17:00" },
"saturday": { "enabled": false, "start": "09:00", "end": "17:00" },
"sunday": { "enabled": false, "start": "09:00", "end": "17:00" }
}
user_profiles
Extended metadata for each user. Linked to auth.users via user_id.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | uuid | NO | gen_random_uuid() | Primary key |
user_id | uuid | NO | FK to auth.users.id (unique) | |
display_name | text | YES | User's display name | |
avatar_url | text | YES | Profile image URL | |
client_id | uuid | YES | FK to clients.id (null for internal users) | |
timezone | text | YES | 'America/New_York' | User's timezone |
availability_settings | jsonb | YES | Weekly availability (same format as clients, used by internal booking) | |
created_at | timestamptz | YES | now() | |
updated_at | timestamptz | YES | now() |
- Internal users (admin, bdr):
client_idisNULL,availability_settingsis used for internal booking links. - Client users:
client_idpoints to their organization. Their availability comes from theclientstable instead.
user_roles
Role assignments linking users to roles. A user can have multiple roles.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | uuid | NO | gen_random_uuid() | Primary key |
user_id | uuid | NO | FK to auth.users.id | |
role | text | NO | Role name: admin, bdr, or client | |
created_at | timestamptz | YES | now() |
Unique constraint: (user_id, role) -- a user cannot have the same role twice.
campaigns
Marketing/outreach campaigns owned by a client. Each campaign targets a specific market of attorneys.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | uuid | NO | gen_random_uuid() | Primary key |
client_id | uuid | NO | FK to clients.id | |
name | text | NO | Campaign name | |
status | text | YES | 'active' | active, paused, completed, draft |
market_id | uuid | YES | FK to markets.id (optional market linkage) | |
description | text | YES | Campaign description | |
start_date | date | YES | Campaign start date | |
end_date | date | YES | Campaign end date | |
created_at | timestamptz | YES | now() | |
updated_at | timestamptz | YES | now() |
contacts
People within campaigns. Typically attorneys being contacted as part of outreach.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | uuid | NO | gen_random_uuid() | Primary key |
campaign_id | uuid | YES | FK to campaigns.id | |
client_id | uuid | YES | FK to clients.id | |
name | text | YES | Full name | |
first_name | text | YES | First name | |
last_name | text | YES | Last name | |
email | text | YES | Email address | |
phone | text | YES | Phone number | |
firm | text | YES | Law firm name | |
title | text | YES | Job title | |
seniority | text | YES | Seniority level | |
linkedin_url | text | YES | LinkedIn profile URL | |
city | text | YES | City | |
state | text | YES | State | |
status | text | YES | 'active' | Contact status |
notes | text | YES | Free-text notes | |
created_at | timestamptz | YES | now() | |
updated_at | timestamptz | YES | now() |
meetings
Scheduled meetings with contacts. Created when someone books via a client booking link.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | uuid | NO | gen_random_uuid() | Primary key |
contact_id | uuid | YES | FK to contacts.id | |
client_id | uuid | YES | FK to clients.id | |
booking_link_id | uuid | YES | FK to booking_links.id | |
title | text | YES | Meeting title | |
scheduled_for | timestamptz | NO | Meeting date/time | |
duration_minutes | integer | YES | 30 | Duration in minutes |
status | text | YES | 'scheduled' | scheduled, completed, cancelled, no_show |
booker_name | text | YES | Name of person who booked | |
booker_email | text | YES | Email of person who booked | |
booker_phone | text | YES | Phone of person who booked | |
booking_source | text | YES | manual or booking_link | |
calendar_event_id | text | YES | ID from Google/Outlook calendar | |
meeting_link | text | YES | Video call URL (Google Meet / Teams) | |
confirmation_sent_at | timestamptz | YES | When confirmation email was sent | |
reminder_24h_sent_at | timestamptz | YES | When 24h reminder was sent | |
reminder_1h_sent_at | timestamptz | YES | When 1h reminder was sent | |
outcome_notes | text | YES | Post-meeting notes | |
created_at | timestamptz | YES | now() | |
updated_at | timestamptz | YES | now() |
calls
Call records for client campaigns. Contains disposition, transcript, recording URL, and AI-generated summary.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | uuid | NO | gen_random_uuid() | Primary key |
contact_id | uuid | YES | FK to contacts.id | |
campaign_id | uuid | YES | FK to campaigns.id | |
client_id | uuid | YES | FK to clients.id | |
disposition | text | YES | Call outcome (see 9 canonical dispositions below) | |
transcript | text | YES | Full call transcript (alias: transcript_raw) | |
recording_url | text | YES | URL to call recording in Supabase Storage | |
duration | integer | YES | Call duration in seconds | |
ai_summary | jsonb | YES | AI-generated summary (key_points, sentiment, etc.) | |
notes | text | YES | BDR notes | |
called_at | timestamptz | YES | When the call happened | |
called_by | uuid | YES | FK to auth.users.id (BDR who made the call) | |
external_call_id | text | YES | GHL call ID (used for deduplication) | |
phone_number | text | YES | Phone number dialed | |
created_at | timestamptz | YES | now() | |
updated_at | timestamptz | YES | now() |
9 Canonical Dispositions (Title Case)
| Disposition | Category | Description |
|---|---|---|
No Answer | Access | Phone rang, nobody picked up |
Voicemail | Access | Reached voicemail |
Gatekeeper | Access | Reached assistant/receptionist, blocked |
Incorrect Number | Access | Wrong number or disconnected |
Not Interested | Conversation | Connected but declined to engage |
Callback Requested | Conversation | Asked to call back later |
Future Potential | Opportunity | Interested but not now |
Meeting Booked | Opportunity | Meeting scheduled |
Do Not Contact | Hard stop | Requested no further contact |
The disposition column is TEXT, not an enum. It was migrated from a call_disposition enum via migration 20260210200001_convert_disposition_to_text.sql. Always use the exact Title Case values above.
notification_preferences
Per-user notification settings controlling which emails and in-app notifications are enabled.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | uuid | NO | gen_random_uuid() | Primary key |
user_id | uuid | NO | FK to auth.users.id (unique) | |
email_meeting_booked | boolean | YES | true | Email when a meeting is booked |
email_meeting_reminder | boolean | YES | true | Email meeting reminders |
email_meeting_cancelled | boolean | YES | true | Email when meeting cancelled |
email_weekly_summary | boolean | YES | true | Weekly summary email |
in_app_new_signal | boolean | YES | true | In-app notification for new signals |
in_app_call_logged | boolean | YES | true | In-app notification for new calls |
created_at | timestamptz | YES | now() | |
updated_at | timestamptz | YES | now() |