# Prisma-to-Convex Mapping Reference

> Single source of truth for debugging Prisma <-> Convex data-source mismatches.
> Generated: 2026-03-04

---

## Table of Contents

1. [Domain -> Route Mapping](#section-1-domain--route-mapping)
2. [Prisma Model -> Convex Table Mapping](#section-2-prisma-model--convex-table-mapping)
3. [Common Field Type Translations](#section-3-common-field-type-translations)
4. [Convex Function Inventory](#section-4-convex-function-inventory)
5. [Gap Analysis](#section-5-gap-analysis)
6. [Common Patterns and Gotchas](#section-6-common-patterns-and-gotchas)
7. [Quick Fix Recipes](#section-7-quick-fix-recipes)

---

## Section 1: Domain -> Route Mapping

All 21 domains are defined in `lib/data-source.ts`. Currently ALL are set to `convex` by default, but can be overridden per-domain via `DS_<DOMAIN>=prisma` environment variable.

| Domain          | API Route Prefixes                                                                                                                                                  | Notes                                                                                                                                                                                                            |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auth`          | `/api/auth/*`, `/api/send-otp`, `/api/register`, `/api/passwordless/*`, `/api/user-preferences`, `/api/user-profile-update`, `/api/user-links`, `/api/onboarding/*` | Users, sessions, OTPs, preferences, onboarding, audit logs                                                                                                                                                       |
| `bookings`      | `/api/bookings/*`, `/api/demo-book`, `/api/staff/dashboard/demos*`                                                                                                  | Bookings, demo bookings, booking notes, staff notes, customers, waitlists                                                                                                                                        |
| `billing`       | `/api/billing-history`, `/api/billing-invoice`, `/api/subscription-*`, `/api/checkout*`, `/api/webhooks/stripe`                                                     | Subscriptions, billing history, payments, refunds, subscription tiers                                                                                                                                            |
| `notifications` | `/api/notifications/*`, `/api/admin/notifications`                                                                                                                  | User notifications, admin notifications, system messages                                                                                                                                                         |
| `support`       | `/api/support/*`, `/api/contact-submit`, `/api/staff/contacts-*`                                                                                                    | Support tickets, ticket messages, FAQs, contact submissions, contact messages, billing emails, email templates                                                                                                   |
| `social`        | `/api/social/*`                                                                                                                                                     | Social accounts, content queue, analytics, messages, trends, posts, AI content generations                                                                                                                       |
| `leads`         | `/api/leads/*`, `/api/crm/*`                                                                                                                                        | Leads, activities, scoring rules/events, pipeline stages, capture forms, nurture sequences/steps/memberships                                                                                                     |
| `quotations`    | `/api/quotations/*`, `/api/deals/*`                                                                                                                                 | Quotations, items, payments, penalties, reminders, settings, recurring templates, deals                                                                                                                          |
| `whatsapp`      | `/api/whatsapp/*`, `/api/webhooks/whatsapp`                                                                                                                         | WhatsApp conversations, conversation messages, org conversations, org messages, user language preferences, company knowledge base                                                                                |
| `voice_agents`  | `/api/voice-agents/*`, `/api/knowledge-base/*`                                                                                                                      | Conversation logs, log messages, voice actions, phone config, KB sync, settings, KB documents, chunks, FAQs, KB settings                                                                                         |
| `content`       | `/api/content/*`, `/api/blog/*`, `/api/staff/blog*`                                                                                                                 | Blog posts, hero sections, feature blocks, footer blocks, CTA sections, FAQ entries, announcements, page layouts, site settings, social proof, content versions, preview tokens, media assets, events, campaigns |
| `careers`       | `/api/careers/*`, `/api/apply-job`, `/api/staff/interview*`                                                                                                         | Careers, jobs, job applications, application notes, interview bookings, interview booking tokens                                                                                                                 |
| `reviews`       | `/api/reviews/*`, `/api/referrals/*`                                                                                                                                | Reviews, review requests, question templates, widget settings, referrals                                                                                                                                         |
| `expenses`      | `/api/expenses/*`                                                                                                                                                   | Expense categories, expenses                                                                                                                                                                                     |
| `real_estate`   | `/api/real-estate/*`, `/api/properties/*`                                                                                                                           | Properties, images, brokers, availability, viewings, tours, feedback, client requirements, matches, transactions                                                                                                 |
| `analytics`     | `/api/analytics/*`                                                                                                                                                  | Analytics events, site visits, workflow analytics, web vitals, workflow errors                                                                                                                                   |
| `organizations` | `/api/organizations/*`, `/api/invitation/*`, `/api/team/*`                                                                                                          | Organizations, members, invitations                                                                                                                                                                              |
| `workflows`     | `/api/workflows/*`, `/api/user-links`                                                                                                                               | User workflows, API keys, user links                                                                                                                                                                             |
| `nurture`       | `/api/nurture/*`                                                                                                                                                    | Email nurture sequences, steps, user states, user emails                                                                                                                                                         |
| `sync`          | `/api/sync/*`, `/api/admin/sync*`                                                                                                                                   | Sync queue, DLQ, DLQ archive                                                                                                                                                                                     |
| `customers`     | `/api/customers/*`                                                                                                                                                  | Customers table (shared with bookings schema)                                                                                                                                                                    |

### Override Example

```bash
# Force bookings domain to use Prisma (instant rollback)
DS_BOOKINGS=prisma

# Force auth back to Prisma
DS_AUTH=prisma
```

---

## Section 2: Prisma Model -> Convex Table Mapping

### Legend

- **Prisma Name -> Convex Name**: Model/table name mapping
- **Schema File**: Which `convex/schema/*.ts` file defines the table
- Fields listed only where there are notable differences

---

### Auth Domain (`convex/schema/auth.ts`)

#### `users` -> `users`

| Prisma Field                      | Convex Field                    | Type Change                           | Notes                        |
| --------------------------------- | ------------------------------- | ------------------------------------- | ---------------------------- |
| `id` (String @id)                 | `_id` (Doc ID)                  | Convex auto-generated                 | Use `prismaId` for cross-ref |
| `emailVerified` (DateTime?)       | `emailVerified` (number?)       | DateTime -> number (ms)               |                              |
| `createdAt` (DateTime)            | `createdAt` (number)            | DateTime -> number (ms)               |                              |
| `updatedAt` (DateTime)            | `updatedAt` (number)            | DateTime -> number (ms)               |                              |
| `lastLoginAt` (DateTime?)         | `lastLoginAt` (number?)         | DateTime -> number (ms)               |                              |
| `lockedUntil` (DateTime?)         | `lockedUntil` (number?)         | DateTime -> number (ms)               |                              |
| `suspendedAt` (DateTime?)         | `suspendedAt` (number?)         | DateTime -> number (ms)               |                              |
| `whatsappConnectedAt` (DateTime?) | `whatsappConnectedAt` (number?) | DateTime -> number (ms)               |                              |
| `goals` (Json?)                   | `goals` (v.any())               | Json -> v.any()                       |                              |
| `voiceAgentConfig` (Json?)        | `voiceAgentConfig` (v.any())    | Json -> v.any()                       |                              |
| `clerkId` (@map "clerk_id")       | `clerkId`                       | Column name difference in Prisma only |                              |
| `preferredCurrency` (@map)        | `preferredCurrency`             | Column name difference in Prisma only |                              |
| Relations (many)                  | N/A                             | Removed                               | No automatic joins in Convex |

#### `otps` -> `otps`

All fields match. DateTime fields -> number.

#### `user_sessions` -> `user_sessions`

| Prisma                   | Convex                     | Notes             |
| ------------------------ | -------------------------- | ----------------- |
| `lastActive` (DateTime)  | `lastActive` (number)      |                   |
| `expiresAt` (DateTime)   | `expiresAt` (number)       |                   |
| `login_history` relation | `loginHistoryId` (string?) | Manual join by ID |

#### `login_history` -> `login_history`

| Prisma                   | Convex               | Notes             |
| ------------------------ | -------------------- | ----------------- |
| `latitude` (Decimal)     | `latitude` (number)  | Decimal -> number |
| `longitude` (Decimal)    | `longitude` (number) | Decimal -> number |
| `user_sessions` relation | N/A                  | Manual join       |

#### `user_preferences` -> `user_preferences`

| Prisma                         | Convex                            | Notes |
| ------------------------------ | --------------------------------- | ----- |
| `emailNotifications` (Json)    | `emailNotifications` (v.any())    |       |
| `smsNotifications` (Json)      | `smsNotifications` (v.any())      |       |
| `whatsappNotifications` (Json) | `whatsappNotifications` (v.any()) |       |
| `dashboardPreferences` (Json)  | `dashboardPreferences` (v.any())  |       |

#### `onboarding_progress` -> `onboarding_progress`

All fields match. DateTimes -> numbers.

#### `audit_logs` -> `audit_logs`

| Prisma             | Convex                | Notes |
| ------------------ | --------------------- | ----- |
| `metadata` (Json?) | `metadata` (v.any()?) |       |

---

### Bookings Domain (`convex/schema/bookings.ts`)

#### `bookings` -> `bookings`

| Prisma                        | Convex                            | Notes                       |
| ----------------------------- | --------------------------------- | --------------------------- |
| `appointmentDate` (DateTime)  | `appointmentDate` (number)        | DateTime -> number (ms)     |
| `depositAmount` (Float?)      | `depositAmount` (number?)         | Float -> number             |
| `totalAmount` (Float)         | `totalAmount` (number)            | Float -> number             |
| `status` (BookingStatus enum) | `status` (bookingStatusValidator) | Enum -> v.union of literals |
| `source` (BookingSource enum) | `source` (bookingSourceValidator) | Enum -> v.union of literals |
| `booking_notes` relation      | N/A                               | Manual join on bookingId    |
| `review_requests` relation    | N/A                               | Manual join on bookingId    |

#### `booking_notes` -> `booking_notes`

All fields match. DateTimes -> numbers.

#### `customers` -> `customers`

All fields match. DateTimes -> numbers.

- **Note**: `customers` table is defined in `convex/schema/bookings.ts`, NOT a separate schema file.
- Relations to `quotations`, `deals`, `review_requests`, `quotation_recurring_templates` are removed (manual join).

#### `demo_bookings` -> `demo_bookings`

All fields match. DateTimes -> numbers. Enums -> v.union of literals.

#### `staff_notes` -> `staff_notes`

All fields match. DateTimes -> numbers. `NoteType` enum -> `noteTypeValidator`.

#### `company_availability` -> `company_availability`

All fields match. DateTimes -> numbers. No Prisma enum (Int for dayOfWeek).

#### `appointment_types` -> `appointment_types`

| Prisma           | Convex            | Notes           |
| ---------------- | ----------------- | --------------- |
| `price` (Float?) | `price` (number?) | Float -> number |

#### `waitlists` -> `waitlists`

All fields match. DateTimes -> numbers.

#### `waitlist_signups` -> `waitlist_signups`

All fields match. DateTimes -> numbers. No `updatedAt` in Convex (Prisma has no updatedAt either on this model).

---

### Billing Domain (`convex/schema/billing.ts`)

#### `subscriptions` -> `subscriptions`

| Prisma                                | Convex                      | Notes       |
| ------------------------------------- | --------------------------- | ----------- |
| `currentPeriodEnd` (DateTime)         | `currentPeriodEnd` (number) |             |
| `setupFeeAmount` (Int?)               | `setupFeeAmount` (number?)  |             |
| Relations to billing_history, refunds | N/A                         | Manual join |

#### `billing_history` -> `billing_history`

| Prisma                   | Convex | Notes                         |
| ------------------------ | ------ | ----------------------------- |
| `subscriptions` relation | N/A    | Manual join by subscriptionId |

#### `payments` -> `payments`

| Prisma             | Convex            | Notes       |
| ------------------ | ----------------- | ----------- |
| `amount` (Float)   | `amount` (number) |             |
| `refunds` relation | N/A               | Manual join |

#### `refunds` -> `refunds`

| Prisma                                      | Convex                            | Notes       |
| ------------------------------------------- | --------------------------------- | ----------- |
| `status` (RefundStatus enum)                | `status` (refundStatusValidator)  |             |
| `reason` (RefundReason enum?)               | `reason` (refundReasonValidator?) |             |
| Relations to payments, subscriptions, users | N/A                               | Manual join |

#### `subscription_tiers` -> `subscription_tiers`

| Prisma            | Convex               | Notes |
| ----------------- | -------------------- | ----- |
| `features` (Json) | `features` (v.any()) |       |

#### `pricing_copy` -> `pricing_copy`

| Prisma       | Convex          | Notes |
| ------------ | --------------- | ----- |
| `faq` (Json) | `faq` (v.any()) |       |

---

### Notifications Domain (`convex/schema/notifications.ts`)

#### `notifications` -> `notifications`

| Prisma                         | Convex                             | Notes |
| ------------------------------ | ---------------------------------- | ----- |
| `type` (NotificationType enum) | `type` (notificationTypeValidator) |       |

#### `admin_notifications` -> `admin_notifications`

| Prisma            | Convex               | Notes                             |
| ----------------- | -------------------- | --------------------------------- |
| `metadata` (Json) | `metadata` (v.any()) | Required in Convex (not optional) |

#### `system_messages` -> `system_messages`

All fields match. DateTimes -> numbers.

#### `system_message_views` -> `system_message_views`

All fields match. DateTimes -> numbers.

#### `staff_alerts` -> `staff_alerts`

| Prisma                          | Convex                              | Notes |
| ------------------------------- | ----------------------------------- | ----- |
| `type` (AlertType enum)         | `type` (alertTypeValidator)         |       |
| `severity` (AlertSeverity enum) | `severity` (alertSeverityValidator) |       |

---

### Organizations Domain (`convex/schema/organizations.ts`)

#### `organizations` -> `organizations`

All fields match. DateTimes -> numbers. Relations removed.

#### `organization_members` -> `organization_members`

| Prisma                | Convex                    | Notes |
| --------------------- | ------------------------- | ----- |
| `role` (OrgRole enum) | `role` (orgRoleValidator) |       |
| `joinedAt` (DateTime) | `joinedAt` (number)       |       |

#### `organization_invitations` -> `organization_invitations`

| Prisma                           | Convex                               | Notes |
| -------------------------------- | ------------------------------------ | ----- |
| `role` (OrgRole enum)            | `role` (orgRoleValidator)            |       |
| `status` (InvitationStatus enum) | `status` (invitationStatusValidator) |       |

---

### Support Domain (`convex/schema/support.ts`)

#### `support_tickets` -> `support_tickets`

Enums -> validators. DateTimes -> numbers.

#### `ticket_messages` -> `ticket_messages`

| Prisma                               | Convex                                   | Notes |
| ------------------------------------ | ---------------------------------------- | ----- |
| `senderType` (TicketSenderType enum) | `senderType` (ticketSenderTypeValidator) |       |

#### `support_faqs` -> `support_faqs`

| Prisma                               | Convex                                   | Notes |
| ------------------------------------ | ---------------------------------------- | ----- |
| `category` (SupportFAQCategory enum) | `category` (supportFAQCategoryValidator) |       |

#### `contact_submissions` -> `contact_submissions`

All fields match. DateTimes -> numbers.

#### `contact_messages` -> `contact_messages`

| Prisma                                     | Convex                                         | Notes |
| ------------------------------------------ | ---------------------------------------------- | ----- |
| `direction` (ContactMessageDirection enum) | `direction` (contactMessageDirectionValidator) |       |
| `status` (ContactMessageStatus enum)       | `status` (contactMessageStatusValidator)       |       |
| `ccEmails` (Json)                          | `ccEmails` (v.any())                           |       |

#### `email_templates` -> `email_templates`

All fields match. DateTimes -> numbers.

#### `billing_emails` -> `billing_emails`

| Prisma                      | Convex                          | Notes             |
| --------------------------- | ------------------------------- | ----------------- |
| `amount` (Decimal?)         | `amount` (number?)              | Decimal -> number |
| `status` (EmailStatus enum) | `status` (emailStatusValidator) |                   |

---

### Social Domain (`convex/schema/social.ts`)

#### `social_accounts` -> `social_accounts`

| Prisma                                  | Convex                                      | Notes |
| --------------------------------------- | ------------------------------------------- | ----- |
| `platform` (SocialPlatform enum)        | `platform` (socialPlatformValidator)        |       |
| `status` (SocialIntegrationStatus enum) | `status` (socialIntegrationStatusValidator) |       |
| `tokenExpiry` (DateTime?)               | `tokenExpiry` (number?)                     |       |

#### `social_content_queue` -> `social_content_queue`

All fields match. Enums -> validators. DateTimes -> numbers.
Extra Convex fields: `calendarColor`, `campaignTag`.

#### `social_analytics` -> `social_analytics`

Extra Convex fields: `reach`, `engagementRate`, `saves`.

#### `social_messages` -> `social_messages`

All fields match. Enums -> validators.

#### `social_trends` -> `social_trends`

All fields match. Enums -> validators.

#### `social_posts` -> `social_posts`

| Prisma                           | Convex                               | Notes |
| -------------------------------- | ------------------------------------ | ----- |
| `status` (SocialPostStatus enum) | `status` (socialPostStatusValidator) |       |

#### `ai_content_generations` -> `ai_content_generations`

All fields match.

---

### Leads Domain (`convex/schema/leads.ts`)

#### `leads` -> `leads`

| Prisma                                        | Convex                       | Notes       |
| --------------------------------------------- | ---------------------------- | ----------- |
| `customFields` (Json)                         | `customFields` (v.any())     |             |
| `metadata` (Json)                             | `metadata` (v.any())         |             |
| `dealValue` (Float?)                          | `dealValue` (number?)        |             |
| `tags` (String[])                             | `tags` (v.array(v.string())) |             |
| Relations to activities, scoring events, etc. | N/A                          | Manual join |

#### `lead_activities` -> `lead_activities`

| Prisma                                         | Convex                             | Notes |
| ---------------------------------------------- | ---------------------------------- | ----- |
| `type` (LeadActivityType... — enum in Prisma?) | `type` (leadActivityTypeValidator) |       |

#### `lead_scoring_rules` -> `lead_scoring_rules`

| Prisma             | Convex                | Notes |
| ------------------ | --------------------- | ----- |
| `condition` (Json) | `condition` (v.any()) |       |

#### `lead_scoring_events` -> `lead_scoring_events`

All fields match. Enums -> validators.

#### `lead_pipeline_stages` -> `lead_pipeline_stages`

All fields match.

#### `lead_capture_forms` -> `lead_capture_forms`

| Prisma            | Convex               | Notes |
| ----------------- | -------------------- | ----- |
| `fields` (Json)   | `fields` (v.any())   |       |
| `styling` (Json?) | `styling` (v.any()?) |       |

#### `lead_nurture_sequences` -> `lead_nurture_sequences`

All fields match. Enums -> validators.

#### `lead_nurture_steps` -> `lead_nurture_steps`

All fields match. Enums -> validators.

#### `lead_nurture_memberships` -> `lead_nurture_memberships`

All fields match. Enums -> validators.

---

### Quotations Domain (`convex/schema/quotations.ts`)

#### `quotations` -> `quotations`

| Prisma                                        | Convex                                            | Notes                          |
| --------------------------------------------- | ------------------------------------------------- | ------------------------------ |
| `status` (QuotationStatus enum)               | `status` (quotationStatusValidator)               |                                |
| `paymentStatus` (QuotationPaymentStatus enum) | `paymentStatus` (quotationPaymentStatusValidator) |                                |
| Decimal fields                                | number                                            | All financial fields as number |
| Relations to items, payments                  | N/A                                               | Manual join                    |

#### `quotation_items` -> `quotation_items`

All fields match. Decimals -> numbers.

#### `quotation_payments` -> `quotation_payments`

| Prisma                        | Convex                            | Notes |
| ----------------------------- | --------------------------------- | ----- |
| `method` (PaymentMethod enum) | `method` (paymentMethodValidator) |       |

#### `quotation_penalties` -> `quotation_penalties`

| Prisma                           | Convex                               | Notes |
| -------------------------------- | ------------------------------------ | ----- |
| `penaltyType` (PenaltyType enum) | `penaltyType` (penaltyTypeValidator) |       |

#### `quotation_reminders` -> `quotation_reminders`

All fields match.

#### `quotation_settings` -> `quotation_settings`

| Prisma                           | Convex                               | Notes |
| -------------------------------- | ------------------------------------ | ----- |
| `penaltyType` (PenaltyType enum) | `penaltyType` (penaltyTypeValidator) |       |
| `reminderSchedule` (Json)        | `reminderSchedule` (v.any())         |       |

#### `quotation_recurring_templates` -> `quotation_recurring_templates`

| Prisma                                | Convex                                    | Notes |
| ------------------------------------- | ----------------------------------------- | ----- |
| `frequency` (RecurringFrequency enum) | `frequency` (recurringFrequencyValidator) |       |
| `status` (RecurringStatus enum)       | `status` (recurringStatusValidator)       |       |

#### `quotation_recurring_template_items` -> `quotation_recurring_template_items`

All fields match.

#### `deals` -> `deals`

| Prisma                   | Convex                       | Notes |
| ------------------------ | ---------------------------- | ----- |
| `stage` (DealStage enum) | `stage` (dealStageValidator) |       |

---

### WhatsApp Domain (`convex/schema/whatsapp.ts`)

#### `whatsapp_conversations` -> `whatsapp_conversations`

| Prisma                             | Convex                                 | Notes |
| ---------------------------------- | -------------------------------------- | ----- |
| `status` (ConversationStatus enum) | `status` (conversationStatusValidator) |       |
| `tags` (String[])                  | `tags` (v.array(v.string()))           |       |
| `aiActions` (Json?)                | `aiActions` (v.any()?)                 |       |

#### `conversation_messages` -> `conversation_messages`

| Prisma                        | Convex                            | Notes |
| ----------------------------- | --------------------------------- | ----- |
| `sender` (MessageSender enum) | `sender` (messageSenderValidator) |       |

#### `org_whatsapp_conversations` -> `org_whatsapp_conversations`

| Prisma                      | Convex                         | Notes |
| --------------------------- | ------------------------------ | ----- |
| `conversationState` (Json?) | `conversationState` (v.any()?) |       |

#### `org_whatsapp_messages` -> `org_whatsapp_messages`

| Prisma                 | Convex                            | Notes |
| ---------------------- | --------------------------------- | ----- |
| `toolsUsed` (String[]) | `toolsUsed` (v.array(v.string())) |       |

#### `user_language_preferences` -> `user_language_preferences`

All fields match.

#### `company_knowledge_base` -> `company_knowledge_base`

| Prisma              | Convex                 | Notes |
| ------------------- | ---------------------- | ----- |
| `embedding` (Json?) | `embedding` (v.any()?) |       |

---

### Voice Agents Domain (`convex/schema/voice_agents.ts`)

#### `ConversationLog` -> `conversation_logs`

| Prisma                       | Convex                         | Notes                    |
| ---------------------------- | ------------------------------ | ------------------------ |
| Model name `ConversationLog` | Table name `conversation_logs` | PascalCase -> snake_case |
| `messages` (Json)            | `messages` (v.any())           |                          |

#### `ConversationLogMessage` -> `conversation_log_messages`

| Prisma                | Convex                | Notes |
| --------------------- | --------------------- | ----- |
| Model name PascalCase | Table name snake_case |       |

#### `VoiceConversationAction` -> `voice_conversation_actions`

PascalCase -> snake_case.

#### `VoiceAgentPhoneConfig` -> `voice_agent_phone_config`

PascalCase -> snake_case. All SIP fields match.

#### `VoiceAgentKBSync` -> `voice_agent_kb_sync`

PascalCase -> snake_case.

#### `VoiceAgentSettings` -> `voice_agent_settings`

PascalCase -> snake_case. Json fields -> v.any().

#### `KnowledgeBaseDocument` -> `knowledge_base_documents`

PascalCase -> snake_case.

#### `KnowledgeBaseChunk` -> `knowledge_base_chunks`

PascalCase -> snake_case.

#### `KnowledgeBaseFAQ` -> `knowledge_base_faqs`

PascalCase -> snake_case.

#### `KnowledgeBaseSettings` -> `knowledge_base_settings`

PascalCase -> snake_case.

---

### Careers Domain (`convex/schema/careers.ts`)

#### `careers` -> `careers`

| Prisma                      | Convex                        | Notes |
| --------------------------- | ----------------------------- | ----- |
| `titleTranslations` (Json)  | `titleTranslations` (v.any()) |       |
| All Json translation fields | v.any()                       |       |

#### `jobs` -> `jobs`

Same pattern: all Json fields -> v.any(). `JobStatus` enum -> `jobStatusValidator`.

#### `job_applications` -> `job_applications`

All fields match. `metadata` (Json?) -> v.any()?.

#### `application_notes` -> `application_notes`

All fields match.

#### `interview_bookings` -> `interview_bookings`

| Prisma                               | Convex                              | Notes                  |
| ------------------------------------ | ----------------------------------- | ---------------------- |
| `preferredDate` (DateTime @db.Date)  | `preferredDate` (number)            | Date-only -> timestamp |
| `preferredTime` (DateTime @db.Time)  | `preferredTime` (number)            | Time-only -> timestamp |
| `confirmedDate` (DateTime? @db.Date) | `confirmedDate` (number?)           |                        |
| `confirmedTime` (DateTime? @db.Time) | `confirmedTime` (number?)           |                        |
| `status` (InterviewStatus enum)      | `status` (interviewStatusValidator) |                        |

#### `interview_booking_tokens` -> `interview_booking_tokens`

All fields match. DateTimes -> numbers.

---

### Reviews Domain (`convex/schema/reviews.ts`)

#### `reviews` -> `reviews`

All fields match. DateTimes -> numbers.

#### `review_requests` -> `review_requests`

All fields match. `questions` (Json?) -> v.any()?.

#### `review_question_templates` -> `review_question_templates`

| Prisma              | Convex                           | Notes                   |
| ------------------- | -------------------------------- | ----------------------- |
| `questions` (Json?) | `questions` (v.any()?)           | Legacy bulk mode        |
| `options` (Json?)   | `options` (v.array(v.string())?) | Stricter type in Convex |

#### `review_widget_settings` -> `review_widget_settings`

All fields match.

#### `referrals` -> `referrals`

All fields match.

---

### Expenses Domain (`convex/schema/expenses.ts`)

#### `expense_categories` -> `expense_categories`

All fields match.

#### `expenses` -> `expenses`

| Prisma            | Convex                       | Notes |
| ----------------- | ---------------------------- | ----- |
| `date` (DateTime) | `date` (number)              |       |
| `tags` (String[]) | `tags` (v.array(v.string())) |       |

---

### Real Estate Domain (`convex/schema/real_estate.ts`)

#### `Property` -> `property`

| Prisma                             | Convex                       | Notes                         |
| ---------------------------------- | ---------------------------- | ----------------------------- |
| Model name `Property` (PascalCase) | Table `property` (lowercase) |                               |
| Multiple enums                     | Multiple validators          | All GCC-specific enums mapped |
| `amenities` (Json)                 | `amenities` (v.any())        |                               |
| `highlights` (Json)                | `highlights` (v.any())       |                               |

#### `PropertyImage` -> `property_image`

PascalCase -> snake_case.

#### `Broker` -> `broker`

| Prisma                       | Convex                          | Notes |
| ---------------------------- | ------------------------------- | ----- |
| `languages` (Json)           | `languages` (v.any())           |       |
| `areaSpecializations` (Json) | `areaSpecializations` (v.any()) |       |
| `workingDays` (Json)         | `workingDays` (v.any())         |       |

#### `BrokerAvailability` -> `broker_availability`

PascalCase -> snake_case.

#### `PropertyViewing` -> `property_viewing`

PascalCase -> snake_case. All enums -> validators.

#### `ViewingTour` -> `viewing_tour`

PascalCase -> snake_case.

#### `ViewingFeedback` -> `viewing_feedback`

PascalCase -> snake_case.

#### `ClientRequirements` -> `client_requirements`

PascalCase -> snake_case. `propertyTypes` (Json) -> v.any().

#### `PropertyMatch` -> `property_match`

PascalCase -> snake_case. `matchReasons` (Json) -> v.any().

#### `RealEstateTransaction` -> `real_estate_transaction`

PascalCase -> snake_case. All enums -> validators.

---

### Content Domain (`convex/schema/content.ts`)

All CMS content tables follow the same pattern:

| Prisma Field                            | Convex Field                      | Notes        |
| --------------------------------------- | --------------------------------- | ------------ |
| `id` (UUID @db.Uuid)                    | `_id` (Convex Doc ID)             | Use prismaId |
| `status` (ContentStatus enum)           | `status` (contentStatusValidator) |              |
| All DateTime fields                     | number                            |              |
| All Json fields (items, sections, etc.) | v.any()                           |              |

Tables: `blog_posts`, `hero_sections`, `feature_blocks`, `footer_blocks`, `cta_sections`, `faq_entries`, `announcements`, `page_layouts`, `site_settings`, `social_proof`, `content_versions`, `preview_tokens`, `media_assets`, `events`, `campaigns`.

**Note**: `blog_posts` does NOT exist in Prisma (managed by Supabase/Convex only). It has a `coverImageStorageId` field of type `v.id("_storage")` which is Convex-specific storage.

---

### Analytics Domain (`convex/schema/analytics.ts`)

#### `analytics_events` -> `analytics_events`

All fields match.

#### `site_visits` -> `site_visits`

All fields match.

#### `WorkflowAnalytics` -> `workflow_analytics`

PascalCase -> snake_case.

#### `web_vitals` (Convex only)

**No Prisma equivalent**. Convex-only table for Core Web Vitals tracking.

#### `WorkflowError` -> `workflow_errors`

PascalCase -> snake_case. `context` (Json) -> v.any().

---

### Workflows Domain (`convex/schema/workflows.ts`)

#### `UserWorkflow` -> `user_workflows`

PascalCase -> snake_case. `config` (Json) -> v.any().

#### `UserAPIKey` -> `user_api_keys`

PascalCase -> snake_case.

#### `user_links` -> `user_links`

| Prisma                         | Convex                             | Notes |
| ------------------------------ | ---------------------------------- | ----- |
| `linkType` (UserLinkType enum) | `linkType` (userLinkTypeValidator) |       |

---

### Nurture Domain (`convex/schema/nurture.ts`)

#### `email_nurture_sequences` -> `email_nurture_sequences`

| Prisma                                    | Convex                                        | Notes |
| ----------------------------------------- | --------------------------------------------- | ----- |
| `sequenceType` (NurtureSequenceType enum) | `sequenceType` (nurtureSequenceTypeValidator) |       |
| `triggerConditions` (Json?)               | `triggerConditions` (v.any()?)                |       |

#### `email_nurture_sequence_steps` -> `email_nurture_sequence_steps`

All fields match.

#### `user_nurture_sequence_states` -> `user_nurture_sequence_states`

| Prisma             | Convex                | Notes |
| ------------------ | --------------------- | ----- |
| `metadata` (Json?) | `metadata` (v.any()?) |       |

#### `user_nurture_sequence_emails` -> `user_nurture_sequence_emails`

| Prisma                             | Convex                                 | Notes |
| ---------------------------------- | -------------------------------------- | ----- |
| `status` (NurtureEmailStatus enum) | `status` (nurtureEmailStatusValidator) |       |

---

### Sync Domain (`convex/schema/sync.ts`)

#### `sync_queue` -> `sync_queue`

| Prisma        | Convex           | Notes |
| ------------- | ---------------- | ----- |
| `data` (Json) | `data` (v.any()) |       |

#### `sync_queue_dlq` -> `sync_queue_dlq`

All fields match.

#### `sync_queue_dlq_archive` -> `sync_queue_dlq_archive`

All fields match.

---

### Misc Domain (`convex/schema/misc.ts`)

#### `company_profiles` -> `company_profiles`

| Prisma                      | Convex                           | Notes      |
| --------------------------- | -------------------------------- | ---------- |
| Uses snake_case field names | Same snake_case                  | Consistent |
| `services` (String[])       | `services` (v.array(v.string())) |            |

#### `system_settings` -> `system_settings`

All fields match.

#### `admin_notes` -> `admin_notes`

| Prisma                         | Convex                             | Notes |
| ------------------------------ | ---------------------------------- | ----- |
| `category` (NoteCategory enum) | `category` (noteCategoryValidator) |       |

#### `integrations` -> `integrations`

| Prisma                            | Convex                                | Notes |
| --------------------------------- | ------------------------------------- | ----- |
| `type` (IntegrationType enum)     | `type` (integrationTypeValidator)     |       |
| `status` (IntegrationStatus enum) | `status` (integrationStatusValidator) |       |
| `config` (Json?)                  | `config` (v.any()?)                   |       |
| `tokenExpiry` (DateTime?)         | `tokenExpiry` (number?)               |       |

---

## Section 3: Common Field Type Translations

| Prisma Type               | Convex Type                                        | Conversion                                                                                            |
| ------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `DateTime`                | `v.number()`                                       | `Date.now()` for creation; `.getTime()` for existing Date objects; `new Date(timestamp)` to read back |
| `DateTime?`               | `v.optional(v.number())`                           | Same as above, with null -> undefined                                                                 |
| `String @id`              | `_id` (auto) + `prismaId` (v.optional(v.string())) | Convex generates its own `_id`; store old Prisma ID in `prismaId`                                     |
| `String @id @db.Uuid`     | `_id` (auto) + `prismaId` (v.optional(v.string())) | Same pattern for UUID IDs                                                                             |
| `Json` / `Json?`          | `v.any()` / `v.optional(v.any())`                  | No schema validation on shape                                                                         |
| `Enum`                    | `v.union(v.literal("A"), v.literal("B"), ...)`     | Each enum value is a literal                                                                          |
| `Decimal` / `@db.Decimal` | `v.number()`                                       | JavaScript number (beware floating point)                                                             |
| `Float`                   | `v.number()`                                       | Direct                                                                                                |
| `Int`                     | `v.number()`                                       | Direct                                                                                                |
| `Boolean`                 | `v.boolean()`                                      | Direct                                                                                                |
| `String`                  | `v.string()`                                       | Direct                                                                                                |
| `String?`                 | `v.optional(v.string())`                           | `null` in Prisma -> `undefined` in Convex                                                             |
| `String[]`                | `v.array(v.string())`                              | Direct                                                                                                |
| `String[] @default([])`   | `v.array(v.string())`                              | Must pass `[]` explicitly (no @default in Convex)                                                     |
| `Relation (1:many)`       | N/A                                                | Manual index query join                                                                               |
| `Relation (1:1)`          | N/A                                                | Manual index query join                                                                               |
| `@unique` constraint      | Index + uniqueness check in mutation               | Convex doesn't enforce unique at schema level (except indexes)                                        |
| `@@unique([a, b])`        | Composite index + manual check                     | Must check in mutation logic                                                                          |
| `@map("column_name")`     | N/A                                                | Convex uses field name directly                                                                       |
| `@db.Text`                | `v.string()`                                       | No distinction                                                                                        |
| `@db.VarChar(N)`          | `v.string()`                                       | No length constraint in Convex                                                                        |
| `@db.Inet`                | `v.string()`                                       | IP as plain string                                                                                    |
| `@db.Timestamptz(6)`      | `v.number()`                                       | Unix ms timestamp                                                                                     |
| `@default(now())`         | Pass `Date.now()` in mutation                      | No automatic defaults                                                                                 |
| `@default(cuid())`        | Convex auto-generates `_id`                        | Or use prismaId bridge                                                                                |
| `@updatedAt`              | Pass `Date.now()` manually                         | Must set in every patch/update                                                                        |

---

## Section 4: Convex Function Inventory

### auth

**Queries** (`auth/queries`):

- `getCurrentUser()` - Get user via Clerk JWT identity
- `getUser({ id })` - Get user by Convex \_id
- `getUserByClerkId({ clerkId })` - Lookup by Clerk user ID
- `getUserByEmail({ email })` - Lookup by email
- `getUserByGoogleId({ googleId })` - Lookup by Google OAuth ID
- `getSession({ userId })` - Get latest session
- `listUserSessions({ userId })` - All sessions (auth required)
- `getLoginHistory({ userId, limit? })` - Paginated login history
- `getUserProfile({ userId })` - User + org memberships
- `getUserPreferences({ userId })` - User preferences
- `getUserLinks({ userId })` - Active user links
- `getAuditLogs({ userId, type?, limit?, offset? })` - User audit logs
- `getOnboardingProgress({ userId })` - Onboarding progress
- `getAuditLogsStaff({ userId?, type?, action?, success?, ip?, startDate?, endDate?, page?, limit? })` - Staff audit log view
- `getAuditLogTypes()` - Distinct audit log types
- `getSecurityStats()` - Login statistics (24h, 7d, 30d)
- `getLoginHistoryAll({ userId?, success?, country?, ip?, startDate?, endDate?, page?, limit? })` - All login history (staff)
- `getLoginCountries()` - Distinct login countries
- `getLoginsByCountry({ days? })` - Aggregated login stats by country

**Mutations** (`auth/mutations`):

- `createUser({ email, name, clerkId?, role? })` - Create user (Clerk auth required)
- `updateUser({ id, name?, phoneNumber?, companyName?, industry?, country?, city?, avatar?, onboardingComplete?, onboardingStep? })` - Update profile (Clerk auth)
- `updateUserNoAuth({ userId, name?, clerkId?, phoneNumber?, companyName?, industry?, companySize?, country?, city?, address?, website?, avatar?, selectedPackage?, goals?, stripeCustomerId?, onboardingComplete?, onboardingStep? })` - Server-side update
- `deleteUser({ id })` - Cascade delete user (Clerk auth)
- `createSession({ userId, token, deviceName, deviceType, ipAddress, userAgent, expiresAt })` - Track login session
- `deleteSession({ id })` - Remove session
- `updateLastLogin({ id, ipAddress?, city?, country? })` - Update login metadata
- `createUserNoAuth({ email, name, clerkId?, prismaId? })` - Idempotent server-side user creation
- `setOnboardingCompleteNoAuth({ email, onboardingComplete, trialEndsAt })` - Mark onboarding complete
- `updatePreferences({ userId, language?, timezone?, dateFormat?, emailNotifications?, emailFrequency?, dashboardPreferences? })` - Upsert preferences
- `changePasswordByEmail({ email, passwordHash })` - Update password
- `updateUserStatus({ userId, isSuspended, isActive, suspendedAt?, suspendedBy?, suspendedReason? })` - Activate/suspend user
- `createAuditLog({ userId?, type, action, success, ip, userAgent?, metadata? })` - Insert audit log
- `updateOnboardingProgress({ userId, step, completed, whatsappMethod? })` - Update onboarding step
- `dismissOnboarding({ userId })` - Dismiss onboarding
- `completeOnboarding({ userId })` - Mark onboarding complete
- `addUserNote({ userId, staffId, content, category, isPrivate })` - Admin note
- `createLoginHistory({ userId, ipAddress, provider, success, userAgent?, failReason?, country?, countryCode?, city?, region?, latitude?, longitude?, timezone? })` - Login history entry

### bookings

**Queries** (`bookings/queries`):

- `getBookings({ userId, status?, limit? })` - User's bookings
- `getById({ id })` - Single booking
- `getByDate({ userId, startDate, endDate })` - Date range bookings
- `getByCustomer({ userId, customerPhone })` - Customer's bookings
- `getUpcoming({ userId, limit? })` - Future confirmed/pending bookings
- `getDemoBookings({ status?, limit?, startDate?, endDate? })` - Demo bookings
- `getDemoBookingsByDate({ startDate, endDate })` - Demo bookings for slot availability
- `getDemoBookingById({ id })` - Single demo booking
- `getDemoBookingByLegacyId({ legacyId })` - Lookup by prismaId
- `getDemoBookingsByEmail({ email })` - Demo bookings by email

**Mutations** (`bookings/mutations`):

- `create({ userId, organizationId?, customerName, customerPhone, customerEmail?, service, appointmentDate, appointmentTime, duration?, staffMember?, totalAmount, notes?, source? })` - New booking
- `updateStatus({ id, status })` - Change booking status
- `addNote({ bookingId, authorId, authorName, content })` - Add booking note
- `cancel({ id, reason? })` - Cancel booking
- `reschedule({ id, appointmentDate, appointmentTime, staffMember? })` - Reschedule
- `createDemoBooking({ fullName, email, telephone, appointmentDate, appointmentTime, duration, specialRequirements?, language, status?, webhookStatus?, webhookRetryCount?, customerTimezone? })` - New demo booking
- `updateDemoBookingCalendarData({ id, googleCalendarEventId?, googleConferenceId?, googleJoinUrl?, meetingLink? })` - Calendar metadata
- `confirmDemoBooking({ id })` - Confirm demo
- `updateDemoBookingStatus({ id, status, contactedAt?, contactedBy?, internalNotes?, meetingLink? })` - Status + metadata
- `updateDemoBooking({ id, ...allFields })` - Generic patch

### billing

**Queries** (`billing/queries`):

- `getSubscription({ userId })` - Active/trialing subscription
- `getActiveSubscription({ userId })` - Active/trialing/past_due subscription
- `getSubscriptionWithHistory({ userId })` - Subscription + billing history (manual join)
- `getSubscriptionByStripeId({ stripeSubscriptionId })` - Lookup by Stripe sub ID
- `getBillingHistory({ customerId, limit? })` - Customer billing history
- `getRefunds({ status?, customerId?, userId?, limit?, offset? })` - Refund records
- `getRefundById({ refundId })` - Single refund (tries prismaId, stripeRefundId, \_id)
- `getRefundablePayments({ customerId, refundWindowDays? })` - Payments eligible for refund
- `getPayments({ customerId, status? })` - Payment records

**Mutations** (`billing/mutations`):

- `createSubscription({ userId, stripeCustomerId, stripeSubscriptionId, stripePriceId, tierId, status, billingPeriod, currentPeriodEnd, trialEndsAt? })` - New subscription
- `updateSubscription({ id, status?, currentPeriodEnd?, cancelAtPeriodEnd?, cancelledAt?, cardBrand?, cardLastFour? })` - Update subscription
- `recordPayment({ customerId, subscriptionId?, invoiceId?, event, amount, currency, metadata? })` - Billing history entry
- `extendTrial({ subscriptionId, newTrialEnd, newCurrentPeriodEnd })` - Extend trial
- `processRefund({ stripeRefundId, stripePaymentIntentId?, customerId, userId?, amount, currency, reason? })` - Record refund
- `cancelRefund({ stripeRefundId })` - Cancel pending refund
- `updateRefundNotes({ refundId, staffNotes })` - Update refund notes

**Actions** (`billing/actions`):

- `handleStripeEvent({ ... })` - Process Stripe webhook event

### notifications

**Queries** (`notifications/queries`):

- `getNotifications({ userId, limit?, isRead? })` - User notifications
- `getUnreadCount({ userId })` - Unread notification count
- `getAdminNotifications({ limit?, isRead? })` - Admin notifications

**Mutations** (`notifications/mutations`):

- `create({ userId, type, title, message, actionUrl?, metadata? })` - New notification
- `markRead({ id })` - Mark single notification read
- `markAllRead({ userId })` - Mark all notifications read
- `deleteNotification({ id })` - Delete notification

### support

**Queries** (`support/queries`):

- `getTickets({ userId, status?, limit? })` - User's tickets
- `getTicketById({ id })` - Single ticket
- `getMessages({ ticketId })` - Ticket messages
- `getFAQs({ category?, isPublished? })` - Support FAQs
- `getContactSubmissions({ status?, limit?, offset?, search? })` - Contact submissions
- `getContactSubmissionById({ id })` - Single contact submission
- `getContactSubmissionsByEmail({ email })` - Contacts by email
- `getTicketsStaff({ status?, category?, priority?, assignedTo?, search?, page?, limit? })` - Staff ticket view
- `getTicketMetrics()` - Ticket counts by status/category
- `getTicketWithMessages({ ticketId })` - Ticket + messages (manual join)

**Mutations** (`support/mutations`):

- `createTicket({ userId, subject, message, category, priority?, attachments? })` - New ticket
- `updateTicket({ id, status?, priority?, category?, resolution? })` - Update ticket
- `addMessage({ ticketId, senderId, senderType, message, attachments? })` - Ticket message
- `updateStatus({ id, status })` - Update contact submission status
- `closeTicket({ id, resolvedBy?, resolution? })` - Close ticket
- `updateContactSubmission({ id, status?, notes?, resolved_at?, resolved_by? })` - Update contact
- `deleteContactSubmission({ id })` - Delete contact
- `createContactSubmission({ name, email, phone, company?, topic, message, preferred_language, consent, ... })` - New contact

### organizations

**Queries** (`organizations/queries`):

- `getOrganization({ id })` - Single organization
- `getMembers({ organizationId })` - Org members
- `getInvitations({ organizationId, status? })` - Org invitations
- `getInvitationByToken({ token })` - Lookup invitation by token
- `getMemberByUserId({ organizationId, userId })` - Single member
- `getOwnerMembership({ organizationId })` - Owner member record

**Mutations** (`organizations/mutations`):

- `createOrganization({ name, ownerId, subscriptionTier? })` - New org
- `addMember({ organizationId, userId, role?, invitedBy? })` - Add member
- `removeMember({ organizationId, userId })` - Remove member
- `acceptInvitation({ token, userId })` - Accept invitation
- `updateMemberRole({ organizationId, userId, role })` - Change role
- `generateApiToken({ organizationId })` - Generate API token
- `revokeApiToken({ organizationId })` - Revoke API token
- `addSeats({ organizationId, additionalSeats })` - Add purchased seats
- `updateCompanyInfo({ organizationId, name?, ... })` - Update org details

### social

**Queries** (`social/queries`):

- `getAccounts({ userId })` - Social accounts
- `getContentQueue({ socialAccountId?, status?, limit? })` - Content queue
- `getAnalytics({ queueItemId })` - Post analytics
- `getMessages({ socialAccountId?, conversationId?, isRead?, limit? })` - Social messages
- `getAccountById({ id })` - Single account
- `getAccountByPlatformId({ userId, platform, platformAccountId })` - Platform lookup
- `getTrends({ userId, trendType?, platform? })` - Social trends

**Mutations** (`social/mutations`):

- `createPost({ socialAccountId, platform, content, hashtags, mediaUrls, status, scheduledAt? })` - Create post
- `schedulePost({ id, scheduledAt })` - Schedule post
- `updatePostStatus({ id, status, platformPostId?, platformUrl?, errorMessage? })` - Update post status
- `markMessageRead({ id })` - Mark message read
- `connectAccount({ userId, platform, platformAccountId, accountName, accessToken, refreshToken?, tokenExpiry?, metadata? })` - Connect social account
- `disconnectAccount({ id })` - Disconnect account
- `deleteAccount({ id })` - Delete account
- `upsertAccount({ userId, platform, platformAccountId, accountName, accessToken, refreshToken?, tokenExpiry?, metadata? })` - Upsert account

**Actions** (`social/actions`):

- `publishToMeta({ ... })` - Publish to Meta platforms
- `fetchAnalytics({ ... })` - Fetch analytics from platforms

### leads

**Queries** (`leads/queries`):

- `getLeads({ userId, status?, limit? })` - User's leads
- `getById({ id })` - Single lead
- `getActivities({ leadId, limit? })` - Lead activities
- `getPipelineStages({ userId })` - Pipeline stages

**Mutations** (`leads/mutations`):

- `createLead({ userId, name, phone?, email?, company?, source, status?, ... })` - New lead
- `updateLead({ id, name?, phone?, email?, company?, status?, ... })` - Update lead
- `assignLead({ id, assignedTo })` - Assign lead
- `updateLeadStatus({ id, status })` - Change status
- `addLeadNote({ leadId, userId, title, description?, metadata? })` - Add note (activity)
- `deleteLead({ id })` - Delete lead
- `movePipelineStage({ id, pipelineStageId })` - Move to pipeline stage
- `addActivity({ leadId, userId, type, title, description?, metadata? })` - Add activity
- `updateScore({ id, score })` - Update lead score

### quotations

**Queries** (`quotations/queries`):

- `getQuotations({ userId, status?, limit? })` - User's quotations
- `getById({ id })` - Single quotation
- `getItems({ quotationId })` - Quotation line items
- `getPayments({ quotationId })` - Quotation payments
- `getByUserAndId({ userId, quotationId })` - Auth-scoped quotation
- `getWithItemsByUserAndId({ userId, quotationId })` - Quotation + items (manual join)
- `getSettings({ userId })` - Quotation settings
- `getDeals({ userId, stage?, limit? })` - User's deals
- `getRecurringQuotations({ userId, status?, limit? })` - Recurring templates
- `getRecurringById({ id })` - Single recurring template
- `getQuotationByPortalToken({ portalToken })` - Public portal lookup

**Mutations** (`quotations/mutations`):

- `createQuotation({ userId, customerId?, quotationNumber, title, notes?, status, currency, subtotal, taxRate, taxAmount, discountRate, discountAmount, total, validUntil?, metadata? })` - New quotation
- `updateQuotation({ id, title?, notes?, status?, validUntil?, ... })` - Update quotation
- `sendQuotation({ id })` - Send quotation
- `acceptQuotation({ id })` - Accept quotation
- `rejectQuotation({ id })` - Reject quotation
- `addItem({ quotationId, description, quantity, unitPrice, total, sortOrder })` - Add line item
- `removeItem({ id })` - Remove line item
- `upsertSettings({ userId, defaultCurrency, defaultTaxRate, ... })` - Upsert settings
- `recordPayment({ quotationId, amount, currency, method, stripePaymentIntentId?, reference?, manualNotes?, recordedBy? })` - Record payment
- `createRecurring({ userId, customerId?, title, ... })` - New recurring template
- `updateRecurring({ id, title?, ... })` - Update recurring
- `cancelRecurring({ id })` - Cancel recurring
- `pauseRecurring({ id })` - Pause recurring
- `resumeRecurring({ id })` - Resume recurring

### whatsapp

**Queries** (`whatsapp/queries`):

- `getWhatsappConfig({ userId })` - User's WhatsApp config (from users table)
- `getKnowledgeBaseEntries({ organizationId, language?, isActive? })` - Company KB entries
- `getConversations({ userId, status?, limit? })` - WhatsApp conversations
- `getMessages({ conversationId })` - Conversation messages
- `getOrgConversations({ organizationId, status?, limit? })` - Org WhatsApp conversations

**Mutations** (`whatsapp/mutations`):

- `updateWhatsappConfig({ userId, whatsappConnected, whatsappNumber?, whatsappConnectedAt? })` - Update WhatsApp config
- `createConversation({ userId, customerPhone, customerName?, status? })` - New conversation
- `handleIncomingMessage({ userId, customerPhone, customerName?, message, sender, metadata? })` - Handle incoming message (upsert conversation + insert message)
- `sendMessage({ conversationId, message, sender?, metadata? })` - Send message
- `markRead({ conversationId })` - Mark conversation read
- `updateConversationStatus({ id, status })` - Update status
- `createOrgConversation({ organizationId, userPhone, userName?, language? })` - New org conversation
- `sendOrgMessage({ conversationId, role, content, language, intent?, toolsUsed?, metadata? })` - Send org message

**Actions** (`whatsapp/actions`):

- `sendViaTwilio({ ... })` - Send WhatsApp via Twilio

### voice_agents

**Queries** (`voice_agents/queries`):

- `getUserByWebhookToken({ token })` - Lookup user by n8n webhook token
- `getUserByVoiceAgentId({ voiceAgentId })` - Lookup user by voice agent ID
- `getUserVoiceAgent({ userId })` - User's voice agent config
- `getVoiceConversationStats({ userId })` - Conversation statistics
- `getConversationLogs({ userId, limit? })` - Conversation logs
- `getPhoneConfig({ userId })` - Phone configuration
- `getKBDocuments({ userId })` - Knowledge base documents

**Mutations** (`voice_agents/mutations`):

- `createConversationLog({ userId, platform, customerPhone?, customerName?, status? })` - New conversation log
- `addMessage({ conversationId, sender, content, metadata? })` - Add message to conversation log
- `updatePhoneConfig({ userId, connectionType?, phoneNumber?, isActive?, ... })` - Update phone config
- `createPhoneConfig({ userId, connectionType, phoneNumber?, isActive, ... })` - Create phone config
- `activatePhoneConfig({ id })` - Activate phone config
- `deactivatePhoneConfig({ id })` - Deactivate phone config
- `updateSettings({ userId, successEvaluationCriteria?, ... })` - Update voice agent settings
- `provisionAgent({ userId, voiceAgentId, voiceAgentConfig })` - Provision voice agent
- `updateConversationLog({ id, status?, lastMessageAt?, lastMessagePreview?, ... })` - Update conversation log
- `createKBDocument({ userId, filename, originalName, ... })` - Create KB document

### careers

**Queries** (`careers/queries`):

- `getJobs({ status?, careerId?, limit? })` - Jobs listing
- `getApplications({ jobId?, status?, limit? })` - Job applications
- `getApplicationById({ id })` - Single application with notes
- `getInterviewBookings({ applicationId?, status?, limit? })` - Interview bookings
- `getInterviewBookingById({ id })` - Single interview booking
- `validateInterviewToken({ token })` - Validate interview token
- `getApplicationNotes({ applicationId })` - Application notes

**Mutations** (`careers/mutations`):

- `createJob({ slug, titleTranslations, department, ... })` - New job
- `updateJobStatus({ id, status })` - Update job status
- `createApplication({ jobId?, applicantName, applicantEmail, applicantPhone, cvPath, cvFilename, ... })` - New application
- `bookInterview({ applicationId, jobId?, tokenId?, applicantName, applicantEmail, ... })` - Book interview
- `cancelInterview({ id, cancelledBy?, cancellationReason? })` - Cancel interview
- `confirmInterview({ id, confirmedBy, confirmedDate?, confirmedTime?, meetingLink?, meetingPlatform? })` - Confirm interview
- `addApplicationNote({ applicationId, staffId, staffName, noteText })` - Add note
- `sendInterviewInvite({ applicationId, applicantEmail, jobId?, expiresAt })` - Create interview token
- `updateApplicationStatus({ id, status, reviewedBy? })` - Update application status

### reviews

**Queries** (`reviews/queries`):

- `getReviews({ userId, rating?, platform?, isFeatured?, limit? })` - User's reviews
- `getReviewRequests({ userId, status?, limit? })` - Review requests
- `getWidgetSettings({ userId })` - Widget settings
- `getReviewStats({ userId })` - Review statistics
- `getQuestionTemplates({ userId, industry?, isActive? })` - Question templates
- `getReviewRequestStats({ userId })` - Request statistics
- `getEligibleBookings({ userId, limit? })` - Bookings eligible for review requests

**Mutations** (`reviews/mutations`):

- `createReview({ userId, customerName, customerEmail?, ... })` - New review
- `createReviewRequest({ userId, customerName, customerEmail?, ... })` - New review request
- `updateReview({ id, responseText?, isFeatured?, featuredAt?, featuredOrder? })` - Update review
- `reorderFeaturedReviews({ updates })` - Reorder featured reviews
- `sendReviewRequest({ id })` - Send review request
- `createQuestionTemplate({ userId, organizationId?, industry?, question?, ... })` - New question template
- `updateQuestionTemplate({ id, question?, questionAr?, ... })` - Update template
- `deleteQuestionTemplate({ id })` - Delete template
- `updateWidgetSettings({ userId, businessName?, primaryColor, backgroundColor, textColor, ... })` - Update widget settings

### expenses

**Queries** (`expenses/queries`):

- `getExpenses({ userId, categoryId?, startDate?, endDate?, limit? })` - User's expenses
- `getCategories({ userId })` - Expense categories

**Mutations** (`expenses/mutations`):

- `createExpense({ userId, categoryId, amount, currency, vendor?, description?, date, ... })` - New expense
- `updateExpense({ id, categoryId?, amount?, currency?, ... })` - Update expense
- `deleteExpense({ id })` - Delete expense
- `createCategory({ userId, name, nameAr?, color, icon?, sortOrder })` - New category

### real_estate

**Queries** (`real_estate/queries`):

- `getProperties({ userId?, organizationId?, status?, limit? })` - Properties
- `getPropertyById({ id })` - Single property with images
- `getViewings({ userId?, organizationId?, brokerId?, propertyId?, status?, limit? })` - Viewings
- `getBrokers({ userId?, organizationId?, isActive? })` - Brokers
- `getClientById({ id })` - Single client requirement
- `getBrokerById({ id })` - Single broker
- `getMatches({ requirementId?, propertyId?, status?, limit? })` - Property matches
- `getBrokerAvailability({ brokerId, date })` - Broker availability
- `getTransactions({ userId?, organizationId?, stage?, limit? })` - Transactions
- `searchProperties({ ...filters })` - Property search with filters
- `getClients({ userId?, organizationId?, isActive? })` - Client requirements
- `getTransactionById({ id })` - Single transaction
- `getViewingById({ id })` - Single viewing with property + broker
- `getViewingAvailability({ propertyId, date })` - Viewing slots
- `getViewingCalendar({ userId?, organizationId?, startDate, endDate })` - Calendar view
- `getViewingReminders({ userId?, organizationId? })` - Upcoming viewing reminders
- `getTours({ userId?, organizationId?, brokerId?, startDate?, endDate? })` - Viewing tours
- `getRealEstateAnalytics({ userId?, organizationId?, period? })` - Real estate analytics

**Mutations** (`real_estate/mutations`):

- `createProperty({ userId, organizationId?, titleEn, titleAr?, ... })` - New property
- `updateProperty({ id, ...fields })` - Update property
- `scheduleViewing({ userId, organizationId?, propertyId, brokerId?, ... })` - Schedule viewing
- `updateViewingStatus({ id, status, ... })` - Update viewing status
- `assignBroker({ viewingId, brokerId })` - Assign broker to viewing
- `createClient({ userId, organizationId?, clientName, clientPhone, ... })` - New client requirement
- `updateClient({ id, ...fields })` - Update client requirement
- `deleteClient({ id })` - Delete client requirement
- `refreshMatches({ requirementId })` - Recalculate property matches
- `createTransaction({ userId, organizationId?, propertyId, clientName, ... })` - New transaction
- `updateTransaction({ id, stage?, offerAmount?, ... })` - Update transaction
- `updateBroker({ id, ...fields })` - Update broker
- `submitViewingFeedback({ viewingId, overallRating?, ... })` - Submit viewing feedback
- `uploadPropertyImage({ propertyId, url, caption?, type?, sortOrder?, isPrimary? })` - Upload property image

### content

**Queries** (`content/queries`):

- `getHeroSections({ lang?, status? })` - Hero sections
- `getFeatureBlocks({ lang?, status? })` - Feature blocks
- `getAnnouncements({ lang?, status?, activeOnly? })` - Announcements
- `getBlogPosts({ status?, language?, tag?, featured?, search?, limit?, offset? })` - Blog posts
- `getBlogPostBySlug({ slug })` - Single blog post
- `getBlogStats()` - Blog statistics
- `getAllBlogTags()` - All blog tags

**Mutations** (`content/mutations`):

- `createContent({ table, lang, version?, ...fields })` - Generic CMS content create
- `updateContent({ table, id, ...fields })` - Generic CMS content update
- `publishContent({ table, id, publishedBy })` - Publish CMS content
- `createBlogPost({ title, titleAr?, slug, content, contentAr?, ... })` - New blog post
- `updateBlogPost({ id, title?, slug?, content?, status?, ... })` - Update blog post
- `deleteBlogPost({ id })` - Delete blog post
- `bulkUpdateBlogStatus({ ids, status })` - Bulk status update

### analytics

**Queries** (`analytics/queries`):

- `getEvents({ eventName?, category?, userId?, startDate?, endDate?, limit? })` - Analytics events
- `getSiteVisits({ page?, userId?, startDate?, endDate?, limit? })` - Site visits
- `getMetricsSummary({ userId?, startDate?, endDate? })` - Metrics summary
- `getWorkflowAnalytics({ userId, workflowType?, startDate?, endDate? })` - Workflow analytics
- `getDashboardMetrics({ userId })` - Dashboard metrics (bookings, revenue, customers, notifications)
- `getDailyStats({ userId, days? })` - Daily booking/revenue stats

**Mutations** (`analytics/mutations`):

- `trackEvent({ eventName, category, properties?, userId?, sessionId?, page? })` - Track event
- `recordVisit({ visitorId, sessionId, page, country?, city?, language?, userAgent?, referrer?, userId? })` - Record visit
- `updateMetrics({ userId, workflowType, messagesReceived?, messagesSent?, appointmentsBooked?, quotationsSent?, leadsGenerated?, metadata? })` - Update workflow metrics
- `recordWebVital({ name, value, rating, url, userAgent?, navigationType? })` - Record web vital
- `upsertWorkflowMetrics({ userId, workflowType, ...metrics })` - Upsert workflow metrics

### workflows

**Queries** (`workflows/queries`):

- `getWorkflows({ userId, status? })` - User's workflows
- `getWorkflowById({ id })` - Single workflow
- `getExecutions({ workflowId?, limit? })` - Workflow executions
- `getPendingJobs({ limit? })` - Pending job queue items

**Mutations** (`workflows/mutations`):

- `createWorkflow({ userId, workflowType, n8nWorkflowId?, config, status? })` - New workflow
- `updateWorkflow({ id, status?, config?, n8nWorkflowId?, healthStatus?, lastErrorAt? })` - Update workflow
- `activateWorkflow({ id })` - Activate workflow
- `deactivateWorkflow({ id })` - Deactivate workflow
- `deleteWorkflow({ id })` - Delete workflow
- `createWorkflowExecution({ ... })` - New execution record
- `updateExecutionStatus({ ... })` - Update execution status
- `processJobQueue({ ... })` - Process job queue

### customers

**Queries** (`customers/queries`):

- `getCustomers({ userId, limit?, search? })` - User's customers
- `getCustomerById({ id })` - Single customer

**Mutations** (`customers/mutations`):

- `createCustomer({ userId, organizationId?, name, phone, email?, notes?, source? })` - New customer
- `updateCustomer({ id, name?, phone?, email?, notes?, source? })` - Update customer
- `deleteCustomer({ id })` - Delete customer

### sync

**Queries** (`sync/queries`):

- `getPendingSyncTasks({ limit? })` - Pending sync tasks
- `getSyncStatus()` - Sync queue status

**Mutations** (`sync/mutations`):

- `createSyncTask({ table, operation, data, conflictColumn?, maxAttempts? })` - New sync task
- `updateSyncTask({ id, attempts?, lastError?, nextRetryAt? })` - Update sync task
- `processSyncQueue({ batchSize? })` - Process sync queue batch

---

## Section 5: Gap Analysis

### Prisma Models with NO Convex Equivalent

These Prisma models do not exist in any Convex schema file:

| Prisma Model | Domain | Notes                                        |
| ------------ | ------ | -------------------------------------------- |
| (none)       |        | All 93 Prisma models have Convex equivalents |

### Convex Tables with NO Prisma Equivalent

| Convex Table | Schema File  | Notes                                    |
| ------------ | ------------ | ---------------------------------------- |
| `web_vitals` | analytics.ts | Convex-only for Core Web Vitals tracking |
| `blog_posts` | content.ts   | Managed in Convex/Supabase, not Prisma   |

### Prisma Relations That Need Manual Join Patterns

Every Prisma relation must be implemented as a manual index lookup in Convex:

| Parent Table                                            | Child Table                                      | Join Pattern                                                                   |
| ------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------ |
| `users` -> `bookings`                                   | `bookings.by_userId`                             | `ctx.db.query("bookings").withIndex("by_userId", q => q.eq("userId", userId))` |
| `users` -> `subscriptions`                              | `subscriptions.by_userId`                        | Same pattern                                                                   |
| `users` -> `notifications`                              | `notifications.by_userId`                        | Same pattern                                                                   |
| `users` -> `leads`                                      | `leads.by_userId`                                | Same pattern                                                                   |
| `bookings` -> `booking_notes`                           | `booking_notes.by_bookingId`                     | Same pattern                                                                   |
| `support_tickets` -> `ticket_messages`                  | `ticket_messages.by_ticketId_createdAt`          | Same pattern                                                                   |
| `quotations` -> `quotation_items`                       | `quotation_items.by_quotationId`                 | Same pattern                                                                   |
| `quotations` -> `quotation_payments`                    | `quotation_payments.by_quotationId`              | Same pattern                                                                   |
| `organizations` -> `organization_members`               | `organization_members.by_organizationId`         | Same pattern                                                                   |
| `organizations` -> `organization_invitations`           | `organization_invitations.by_organizationId`     | Same pattern                                                                   |
| `jobs` -> `job_applications`                            | `job_applications.by_jobId`                      | Same pattern                                                                   |
| `job_applications` -> `interview_bookings`              | `interview_bookings.by_applicationId`            | Same pattern                                                                   |
| `whatsapp_conversations` -> `conversation_messages`     | `conversation_messages.by_conversationId`        | Same pattern                                                                   |
| `org_whatsapp_conversations` -> `org_whatsapp_messages` | `org_whatsapp_messages.by_conversationId`        | Same pattern                                                                   |
| `ConversationLog` -> `ConversationLogMessage`           | `conversation_log_messages.by_conversationId`    | Same pattern                                                                   |
| `leads` -> `lead_activities`                            | `lead_activities.by_leadId`                      | Same pattern                                                                   |
| `leads` -> `lead_scoring_events`                        | `lead_scoring_events.by_leadId`                  | Same pattern                                                                   |
| `KnowledgeBaseDocument` -> `KnowledgeBaseChunk`         | `knowledge_base_chunks.by_documentId_chunkIndex` | Same pattern                                                                   |

### Missing CRUD Functions

Tables that exist in Convex schema but lack complete CRUD in their mutations/queries:

| Table                          | Has Create | Has Read | Has Update | Has Delete | Missing                                    |
| ------------------------------ | ---------- | -------- | ---------- | ---------- | ------------------------------------------ |
| `otps`                         | No         | No       | No         | No         | All CRUD (handled by Redis in auth flow)   |
| `login_history`                | Yes        | Yes      | No         | No         | Update, Delete (append-only is fine)       |
| `admin_notifications`          | No         | Yes      | No         | No         | Create, Update, Delete                     |
| `system_messages`              | No         | No       | No         | No         | All CRUD                                   |
| `system_message_views`         | No         | No       | No         | No         | All CRUD                                   |
| `staff_alerts`                 | No         | No       | No         | No         | All CRUD                                   |
| `company_availability`         | No         | No       | No         | No         | All CRUD (managed via org WhatsApp setup)  |
| `appointment_types`            | No         | No       | No         | No         | All CRUD                                   |
| `waitlists`                    | No         | No       | No         | No         | All CRUD                                   |
| `waitlist_signups`             | No         | No       | No         | No         | All CRUD                                   |
| `company_profiles`             | No         | No       | No         | No         | All CRUD                                   |
| `system_settings`              | No         | No       | No         | No         | All CRUD                                   |
| `integrations`                 | No         | No       | No         | No         | All CRUD                                   |
| `billing_emails`               | No         | No       | No         | No         | All CRUD                                   |
| `email_templates`              | No         | No       | No         | No         | All CRUD                                   |
| `contact_messages`             | No         | No       | No         | No         | All CRUD                                   |
| `lead_scoring_rules`           | No         | No       | No         | No         | All CRUD                                   |
| `lead_pipeline_stages`         | No         | Yes      | No         | No         | Create, Update, Delete                     |
| `lead_capture_forms`           | No         | No       | No         | No         | All CRUD                                   |
| `lead_nurture_sequences`       | No         | No       | No         | No         | All CRUD (in leads domain)                 |
| `lead_nurture_steps`           | No         | No       | No         | No         | All CRUD                                   |
| `lead_nurture_memberships`     | No         | No       | No         | No         | All CRUD                                   |
| `quotation_penalties`          | No         | No       | No         | No         | All CRUD                                   |
| `quotation_reminders`          | No         | No       | No         | No         | All CRUD                                   |
| `subscription_tiers`           | No         | No       | No         | No         | All CRUD (read-only config?)               |
| `pricing_copy`                 | No         | No       | No         | No         | All CRUD (managed by content domain)       |
| `email_nurture_sequences`      | No         | No       | No         | No         | All CRUD                                   |
| `email_nurture_sequence_steps` | No         | No       | No         | No         | All CRUD                                   |
| `user_nurture_sequence_states` | No         | No       | No         | No         | All CRUD                                   |
| `user_nurture_sequence_emails` | No         | No       | No         | No         | All CRUD                                   |
| `sync_queue_dlq`               | No         | No       | No         | No         | All CRUD                                   |
| `sync_queue_dlq_archive`       | No         | No       | No         | No         | All CRUD                                   |
| `content_versions`             | No         | No       | No         | No         | All CRUD                                   |
| `preview_tokens`               | No         | No       | No         | No         | All CRUD                                   |
| `media_assets`                 | No         | No       | No         | No         | All CRUD                                   |
| `events`                       | No         | No       | No         | No         | All CRUD                                   |
| `campaigns`                    | No         | No       | No         | No         | All CRUD                                   |
| `voice_agent_kb_sync`          | No         | No       | No         | No         | All CRUD                                   |
| `broker_availability`          | No         | Yes      | No         | No         | Create, Update, Delete                     |
| `viewing_tour`                 | No         | Yes      | No         | No         | Create, Update, Delete                     |
| `viewing_feedback`             | Yes        | No       | No         | No         | Read, Update, Delete                       |
| `property_match`               | Yes        | Yes      | No         | No         | Update, Delete                             |
| `property_image`               | Yes        | No       | No         | No         | Read (via getPropertyById), Update, Delete |
| `referrals`                    | No         | No       | No         | No         | All CRUD                                   |

---

## Section 6: Common Patterns and Gotchas

### Converting Prisma `findMany` with `where`, `orderBy`, `take`, `skip`

```typescript
// Prisma
const bookings = await prisma.bookings.findMany({
  where: { userId, status: "confirmed" },
  orderBy: { appointmentDate: "desc" },
  take: 10,
  skip: 20,
});

// Convex — use index for first filter, in-memory for the rest
const allBookings = await ctx.db
  .query("bookings")
  .withIndex("by_userId_status", (q) =>
    q.eq("userId", userId).eq("status", "confirmed"),
  )
  .order("desc")
  .collect();

// Manual skip/take
const bookings = allBookings.slice(20, 30);
```

**Key rule**: Convex `.withIndex()` can only filter on leading fields of the index. Use `.filter()` for additional conditions, or collect and filter in memory.

### Converting Prisma `include` (relations) -> Convex manual joins

```typescript
// Prisma
const ticket = await prisma.support_tickets.findUnique({
  where: { id },
  include: { ticket_messages: true },
});

// Convex — two queries
const ticket = await ctx.db.get(id);
const messages = await ctx.db
  .query("ticket_messages")
  .withIndex("by_ticketId_createdAt", (q) => q.eq("ticketId", id))
  .collect();
return { ...ticket, ticket_messages: messages };
```

### Prisma transactions -> Convex mutation atomicity

Convex mutations are **automatically atomic** within a single mutation function. No explicit transaction wrapper needed.

```typescript
// Prisma
await prisma.$transaction([
  prisma.bookings.update({ where: { id }, data: { status: "confirmed" } }),
  prisma.notifications.create({ data: { ... } }),
]);

// Convex — just do both in one mutation
export const confirmBooking = mutation({
  handler: async (ctx, args) => {
    await ctx.db.patch(args.id, { status: "confirmed", updatedAt: Date.now() });
    await ctx.db.insert("notifications", { ... });
    // Both are atomic — either both succeed or both roll back
  },
});
```

### Date handling

```typescript
// Creating a record
const createdAt = Date.now(); // Unix ms — always use this

// Converting Prisma DateTime to Convex number
const convexTimestamp = prismaDate.getTime();

// Converting Convex number back to Date for API responses
const jsDate = new Date(convexTimestamp);

// ISO string for API responses
const isoString = new Date(convexTimestamp).toISOString();

// GOTCHA: Prisma @db.Date (date-only) and @db.Time (time-only)
// In Convex, both are stored as full Unix ms timestamps
// For interview_bookings.preferredDate: store as midnight UTC of that date
// For interview_bookings.preferredTime: store as ms since midnight
```

### ID handling: The `prismaId` bridge pattern

```typescript
// Looking up a record by its legacy Prisma ID
const record = await ctx.db
  .query("demo_bookings")
  .withIndex("by_prismaId", (q) => q.eq("prismaId", legacyId))
  .unique();

// Or filter approach (less efficient, no dedicated index on all tables)
const record = await ctx.db
  .query("users")
  .filter((q) => q.eq(q.field("prismaId"), prismaId))
  .first();

// When creating records during migration, always store prismaId
await ctx.db.insert("bookings", {
  prismaId: prismaRecord.id, // Store the old ID
  ...otherFields,
});
```

### Null vs undefined differences

```typescript
// Prisma: null means "no value"
// Convex: undefined means "no value" — null is NOT valid for optional fields

// Converting Prisma result to Convex-compatible format:
const convexData = {
  notes: prismaRecord.notes ?? undefined, // null -> undefined
  email: prismaRecord.email ?? undefined,
};

// GOTCHA: Convex v.optional(v.string()) accepts undefined but NOT null
// This will throw: await ctx.db.insert("bookings", { notes: null })
// This works:     await ctx.db.insert("bookings", { notes: undefined })
// This also works: omit the field entirely from the insert object
```

### String path convention for convexQuery/convexMutation

```typescript
// The ConvexHttpClient in lib/convex-client.ts uses string paths:
import { convexQuery, convexMutation } from '@/lib/convex-client';

// Query
const user = await convexQuery("auth/queries:getUserByEmail", { email });

// Mutation
await convexMutation("bookings/mutations:create", {
  userId: "...",
  customerName: "John",
  // ...
});

// Action
await convexAction("billing/actions:handleStripeEvent", { ... });

// The path format is: "<domain>/<type>:<functionName>"
// - domain: auth, bookings, billing, etc.
// - type: queries, mutations, actions
// - functionName: the exported const name
```

---

## Section 7: Quick Fix Recipes

### "Cannot read property of null" after Convex query

**Cause**: Convex query returned `null` (record not found), and code tries to access `.field`.

```typescript
// BAD
const user = await convexQuery("auth/queries:getUserByEmail", { email });
const name = user.name; // CRASH if user is null

// GOOD
const user = await convexQuery("auth/queries:getUserByEmail", { email });
if (!user) {
  return NextResponse.json({ error: "User not found" }, { status: 404 });
}
const name = user.name;
```

### Type mismatch on dates

**Cause**: API returns Convex timestamps (numbers) but frontend/Prisma code expects Date objects or ISO strings.

```typescript
// Convex returns: { createdAt: 1709553600000 }
// Frontend expects: { createdAt: "2024-03-04T12:00:00.000Z" }

// Fix in API route — convert before responding:
const bookings = await convexQuery("bookings/queries:getBookings", { userId });
const formatted = bookings.map((b) => ({
  ...b,
  id: b.prismaId || b._id, // Bridge ID
  createdAt: new Date(b.createdAt).toISOString(),
  updatedAt: new Date(b.updatedAt).toISOString(),
  appointmentDate: new Date(b.appointmentDate).toISOString(),
}));
```

### Missing relation data

**Cause**: Prisma `include` was used to eagerly load relations; Convex has no automatic includes.

```typescript
// Prisma (old code):
const ticket = await prisma.support_tickets.findUnique({
  where: { id },
  include: { ticket_messages: true, users: true },
});

// Convex (fix — add manual joins):
const ticket = await convexQuery("support/queries:getTicketById", { id });
if (!ticket) return null;

const messages = await convexQuery("support/queries:getMessages", {
  ticketId: ticket._id,
});
const user = await convexQuery("auth/queries:getUser", { id: ticket.userId });

return { ...ticket, ticket_messages: messages, users: user };
```

### "Function not found" when calling Convex

**Cause**: The Convex function was added/renamed but `_generated/api.ts` was not regenerated.

```bash
# Fix: Regenerate the Convex generated files
CONVEX_DEPLOYMENT=dev:tacit-chinchilla-978 npx convex dev --once

# Verify the function exists in the generated API
grep "functionName" convex/_generated/api.d.ts
```

### "Argument ... is not a valid ID for table ..."

**Cause**: Passing a Prisma ID (cuid/uuid string) where Convex expects a Convex document ID.

```typescript
// BAD — passing Prisma ID as Convex ID
await convexMutation("bookings/mutations:updateStatus", {
  id: prismaBookingId, // "clx1234..." — this is NOT a valid Convex ID
  status: "confirmed",
});

// GOOD — look up by prismaId first, then use _id
const booking = await convexQuery("bookings/queries:getDemoBookingByLegacyId", {
  legacyId: prismaBookingId,
});
if (!booking) throw new Error("Booking not found");

await convexMutation("bookings/mutations:updateStatus", {
  id: booking._id, // This is the valid Convex _id
  status: "confirmed",
});
```

### Enum value not accepted by Convex validator

**Cause**: Prisma enum values and Convex literal unions must match exactly (case-sensitive).

```typescript
// Prisma enum BookingStatus { pending, confirmed, cancelled, completed, no_show }
// Convex: v.union(v.literal("pending"), v.literal("confirmed"), ...)

// BAD — wrong case
await convexMutation("bookings/mutations:create", {
  status: "Pending", // WRONG — Convex expects lowercase "pending"
});

// GOOD
await convexMutation("bookings/mutations:create", {
  status: "pending", // Matches the v.literal exactly
});
```

### convexQuery returns null when Convex is not configured

**Cause**: `convexQuery` returns `null` (not throws) when `NEXT_PUBLIC_CONVEX_URL` is not set.

```typescript
// The convex-client.ts returns null if no URL configured
const result = await convexQuery("auth/queries:getUserByEmail", { email });
// result is null if Convex URL not set — NOT an error

// Fix: Check isConvexConfigured() first, or handle null gracefully
import { isConvexConfigured } from "@/lib/convex-client";

if (!isConvexConfigured()) {
  // Fall back to Prisma
  return prisma.users.findUnique({ where: { email } });
}
```

### @default values not applied in Convex

**Cause**: Prisma `@default(now())`, `@default(0)`, `@default("{}")` are database-level defaults. Convex has no schema defaults.

```typescript
// BAD — missing required fields
await ctx.db.insert("bookings", {
  userId,
  customerName: "John",
  // Missing: status, depositPaid, depositReminderCount, createdAt, updatedAt
});

// GOOD — explicitly set all defaults
await ctx.db.insert("bookings", {
  userId,
  customerName: "John",
  status: "pending", // @default(pending)
  depositPaid: false, // @default(false)
  depositReminderCount: 0, // @default(0)
  createdAt: Date.now(), // @default(now())
  updatedAt: Date.now(), // @updatedAt
  source: "manual", // @default(manual)
  duration: 30, // @default(30)
  // ... all other required fields
});
```

### Sorting in Convex vs Prisma

```typescript
// Prisma: orderBy: { createdAt: 'desc' }
// Convex: .order("desc") — but this sorts by _creationTime (document creation order)

// To sort by a specific field, you need an index on that field:
const results = await ctx.db
  .query("bookings")
  .withIndex("by_userId_createdAt", (q) => q.eq("userId", userId))
  .order("desc") // This sorts by the index fields (userId, createdAt)
  .take(10);

// GOTCHA: .order("desc") without .withIndex() sorts by _creationTime
// not by createdAt or any other field
```

---

## Appendix: Convex Client API Reference

**File**: `lib/convex-client.ts`

```typescript
// Query (read-only, no side effects)
convexQuery<T>(functionRef: string, args?: Record<string, unknown>): Promise<T | null>

// Mutation (read-write, atomic)
convexMutation<T>(functionRef: string, args?: Record<string, unknown>): Promise<T | null>

// Action (can call external APIs, not atomic)
convexAction<T>(functionRef: string, args?: Record<string, unknown>): Promise<T | null>

// Check if Convex is configured
isConvexConfigured(): boolean

// Get raw client (for advanced use)
getConvexClient(): ConvexHttpClient | null
```

**Function path format**: `"<domain>/<type>:<functionName>"`

Examples:

- `"auth/queries:getUserByEmail"`
- `"bookings/mutations:create"`
- `"billing/actions:handleStripeEvent"`
- `"notifications/mutations:markRead"`

**File**: `lib/data-source.ts`

```typescript
// Check which backend a domain uses
getDataSource(domain: DomainName): 'prisma' | 'convex'
isConvex(domain: DomainName): boolean
isPrisma(domain: DomainName): boolean
getAllDataSources(): Record<DomainName, 'prisma' | 'convex'>
```

**Environment override**: `DS_<DOMAIN>=prisma` or `DS_<DOMAIN>=convex`
