Data Model & Schema
Every table, field, relationship, and enum in the CRM database
Internal engineering documentation for the GPS Leaders CRM V2 data model.
- Source of truth:
prisma/schema.prisma - Database: PostgreSQL
- Tenancy: Single-tenant — one operator org (GPS Leaders), a GPS tracking device sales business.
- Generated client output:
src/generated/prisma - Totals: 29 models, 26 enums.
This document describes the schema exactly as defined. All field types, nullability, defaults, and @map column names are taken directly from the schema. Conventions used below:
?after a type means the field/relation is nullable / optional.- "Column" is the physical Postgres column name (
@map). Where omitted, the column name equals the field name. - "Table" is the physical Postgres table name (
@@map). - All
idfields areString @id @default(uuid())unless noted otherwise. createdAt/updatedAtareDateTimewith@default(now())and@updatedAtrespectively, mapped tocreated_at/updated_at. These are noted per model but not re-explained.
Entity-Relationship Overview
┌──────────┐
│ User │ (auth + attribution; AI_AGENT supported)
└────┬─────┘
assignedRep / author / invitedBy / createdBy
│
┌────────────────────────┼───────────────────────────────┐
│ │ │
┌────▼─────┐ status enum ┌───▼────┐ contactId (req) ┌────▼─────┐
│ Company │◄──────────────│ Lead │─────────────────────►│ Contact │
│ (MASTER) │ companyId? │(pursuit│ companyId? │ │
└────┬─────┘ │ record)│ └────┬─────┘
│ └───┬────┘ │
│ 1─N │ 1─N │ 1─N
│ │ │
┌─────┼──────────┐ ┌────────┼──────────┐ ┌──────────┼─────────┐
│ │ │ │ │ │ │ │ │
addresses notes invoices tags tasks sequenceRuns smsMessages callLogs conversations
│ devices emailThreads leadScore bookings
│ fulfillmentOrders activities
│
CompanyAddress ──billTo/shipTo──► Invoice ──1─N──► InvoiceLineItem
│ ──1─N──► Payment
│ ──1─N──► FulfillmentOrder ──1─N──► FulfillmentItem
│ ──1─N──► Device │
│ Device / SimCard
ref tables: AgreementTemplate, TaxRate, Backend,
Carrier, ShippingType, PaymentTerm, ContractTerm
SequenceDefinition ──1─N──► SequenceRun ──1─N──► SequenceStepLog
│ ──1─N──► Task
Inventory: Device ◄──1:1──► SimCard Both linked to FulfillmentItem.
Cross-cutting: Activity = immutable audit log (leadId?/companyId?/contactId?/userId? all nullable)
TaxonomyValue = data-driven dropdown lists (industry/source/vendor/etc.)
Tag / LeadTag = disposition & classification tagging on leads
Setting / MessageTemplate / CallList / VoicemailDrop = config & inbox tooling
The Company / Lead / Contact triangle (core architecture)
This is the most important architectural concept in the schema:
Companyis the MASTER record for any business the org deals with. Whatever the business is — a prospect, a paying customer, a referral partner — there is oneCompanyrow for it. Itsstatusenum (CompanyStatus:LEAD,CUSTOMER,PARTNER,CONTACT,INACTIVE,DEAD) is what classifies the record. A company is not deleted when it stops being a lead; its status changes.Leadis a separate pipeline-pursuit record. ALeadrepresents an active sales pursuit moving through the pipeline (LeadStage). It is not the business itself — it is the deal/effort. ALeadrequires aContact(contactIdis non-null) and optionally references aCompany(companyIdis nullable, because a lead may be sourced — e.g. a Google Maps / Plusvibe import — before aCompanymaster record is established or matched).Contactis a person. A contact optionally belongs to aCompany. Contacts hold the channel identity (email, phone E.164, SMS eligibility) used by communications.- Most operational records (
Note,Task,Activity,Invoice, communications) attach to both aLead(optional) and aCompany.Companyis the durable anchor;Leadis the time-bounded pursuit context.
The Activity audit trail
Activity is an append-only, immutable audit log. It has no updatedAt — rows are written, never edited. Critically, Activity.userId is nullable: this allows attribution to non-human / system actors (background jobs, sequence worker, webhooks, AI agents) where there is no User. All four subject FKs (leadId, companyId, contactId, userId) are nullable so an activity can be scoped to whatever combination of entities applies.
Data-driven taxonomy
Dropdown / classification lists (industry, lead source, vendor, cellular carrier, backend, payment interval, shipping type, payment method) are not hard-coded enums. They live in the TaxonomyValue table, keyed by TaxonomyType. Application code stores the chosen string value on records like Company.industry, Company.source, Company.vendor, Contact.source, Lead.source. This lets the operator add/reorder/deactivate options without a schema migration. (Note: invoice-related reference data — backends, carriers, shipping types, payment/contract terms, tax rates, agreement templates — uses dedicated relational tables with proper FKs, not TaxonomyValue.)
Auth
User
Purpose: A login account / actor in the CRM. Used for authentication, ownership (assigned rep), authorship (notes, templates), and attribution. The AI_AGENT role value means a "user" may be a non-human agent seat — the schema is intentionally kept open for the future org/agent-fleet direction.
Table: users
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
email | String | no | — | email |
passwordHash | String | no | — | password_hash |
firstName | String | no | — | first_name |
lastName | String | no | — | last_name |
role | UserRole | no | OPERATOR | role |
phone | String | yes | — | phone |
isActive | Boolean | no | true | is_active |
invitedById | String | yes | — | invited_by_id |
invitedAt | DateTime | yes | — | invited_at |
lastLoginAt | DateTime | yes | — | last_login_at |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
invitedBy→User?self-relation"UserInvited"viainvitedById, onDelete: SetNull.invitees→User[](back-relation of"UserInvited").tasks→Task[](assigned tasks).activities→Activity[].notes→Note[](authored).messageTemplates→MessageTemplate[](created).assignedCompanies→Company[]via relation"CompanyAssignedRep".emailAccounts→EmailAccount[].smsGateways→SmsGateway[].conversations→Conversation[].
Constraints / indexes: email is @unique.
Core CRM
Company
Purpose: The master record for a business. Every business the org touches (prospect, customer, partner, etc.) has exactly one Company. Its status field classifies what the company currently is. Companies hold firmographic data (fleet size, industry, ICP), enrichment data scraped from Google Maps (rating, review count, hero photo, place ID), and external-system links (qbCustomerId for QuickBooks, stripeCustomerId for Stripe).
Table: companies
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
name | String | no | — | name |
normalizedName | String | no | — | normalized_name |
website | String | yes | — | website |
phone | String | yes | — | phone |
industry | String | yes | — | industry |
icpTag | String | yes | — | icp_tag |
businessLabel | String | yes | — | business_label |
fleetSize | Int | yes | — | fleet_size |
googleRating | Decimal(2,1) | yes | — | google_rating |
googleReviewCount | Int | yes | — | google_review_count |
heroPhotoUrl | String | yes | — | hero_photo_url |
googleMapsUrl | String | yes | — | google_maps_url |
aboutDescription | String | yes | — | about_description |
googlePlaceId | String | yes | — | google_place_id |
linkedinCompanyUrl | String | yes | — | linkedin_company_url |
ownerName | String | yes | — | owner_name |
source | String | yes | — | source |
vendor | String | yes | — | vendor |
salesNote | String | yes | — | sales_note |
taxable | Boolean | no | true | taxable |
assignedRepId | String | yes | — | assigned_rep_id |
qbCustomerId | String | yes | — | qb_customer_id |
stripeCustomerId | String | yes | — | stripe_customer_id |
status | CompanyStatus | no | LEAD | status |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
contacts→Contact[].addresses→CompanyAddress[].leads→Lead[].devices→Device[].invoices→Invoice[].fulfillmentOrders→FulfillmentOrder[].tasks→Task[].activities→Activity[].notes→Note[].assignedRep→User?via relation"CompanyAssignedRep"(assignedRepId), onDelete: SetNull.
Constraints / indexes: googlePlaceId is @unique. Indexes on normalizedName, icpTag, status, assignedRepId.
normalizedNameis a dedupe/match key (case- and punctuation-folded company name);googlePlaceIduniqueness prevents re-importing the same Google Maps business twice.
Contact
Purpose: A person associated with a business — the human you actually email, call, or text. Holds verified channel identity. Optionally belongs to a Company. A Contact is required by every Lead.
Table: contacts
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
companyId | String | yes | — | company_id |
firstName | String | yes | — | first_name |
lastName | String | yes | — | last_name |
fullName | String | yes | — | full_name |
title | String | yes | — | title |
email | String | yes | — | email |
emailNormalized | String | yes | — | email_normalized |
emailStatus | String | yes | — | email_status |
emailVerifiedAt | DateTime | yes | — | email_verified_at |
phone | String | yes | — | phone |
phoneE164 | String | yes | — | phone_e164 |
phoneType | String | yes | — | phone_type |
phoneVerifiedAt | DateTime | yes | — | phone_verified_at |
isSmsEligible | Boolean | no | false | is_sms_eligible |
timezone | String | yes | — | timezone |
linkedinUrl | String | yes | — | linkedin_url |
source | String | yes | — | source |
plusvibeLeadId | String | yes | — | plusvibe_lead_id |
notes | String | yes | — | notes |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
company→Company?viacompanyId(no explicitonDelete; defaults apply — restrict, sincecompanyIdis optional Prisma usesSetNullsemantics by default for optional relations).leads→Lead[].smsMessages→SmsMessage[].callLogs→CallLog[].conversations→Conversation[].activities→Activity[].bookings→Booking[].
Constraints / indexes: emailNormalized is @unique (prevents duplicate contacts by normalized email). Indexes on companyId, phoneE164.
Many Google-Maps-sourced leads have no contact name — that is why
firstName,lastName, andfullNameare all nullable.plusvibeLeadIdlinks the contact back to the Plusvibe outbound system.
Lead
Purpose: A pipeline-pursuit record — an active sales effort moving through stages. Distinct from Company (the master business record). A lead must have a Contact; it may have a Company. Tracks stage, estimated deal value, win/loss, and outbound-automation flags (Plusvibe pause/stop, DNC).
Table: leads
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
companyId | String | yes | — | company_id |
contactId | String | no | — | contact_id |
stage | LeadStage | no | NEW_LEAD | stage |
source | String | yes | — | source |
sourceCampaign | String | yes | — | source_campaign |
estimatedUnits | Int | yes | — | estimated_units |
estimatedRevenue | Decimal(10,2) | yes | — | estimated_revenue |
lostReason | String | yes | — | lost_reason |
wonAt | DateTime | yes | — | won_at |
lostAt | DateTime | yes | — | lost_at |
plusvibePaused | Boolean | no | false | plusvibe_paused |
plusvibeStopped | Boolean | no | false | plusvibe_stopped |
isDnc | Boolean | no | false | is_dnc |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
company→Company?viacompanyId.contact→ContactviacontactId(required).tags→LeadTag[].tasks→Task[].activities→Activity[].emailThreads→EmailThread[].smsMessages→SmsMessage[].callLogs→CallLog[].conversations→Conversation[].sequenceRuns→SequenceRun[].invoices→Invoice[].leadScore→LeadScore?(1:1).notes→Note[].bookings→Booking[].
Constraints / indexes: Indexes on companyId, contactId, stage, source, createdAt.
Note
Purpose: A free-text note authored by a user against a company (and optionally a specific lead). Used for sales commentary and history.
Table: notes
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
companyId | String | no | — | company_id |
leadId | String | yes | — | lead_id |
authorId | String | no | — | author_id |
body | String @db.Text | no | — | body |
createdAt | DateTime | no | now() | created_at |
editedAt | DateTime | yes | — | edited_at |
Relationships:
company→CompanyviacompanyId, onDelete: Cascade.lead→Lead?vialeadId, onDelete: SetNull.author→UserviaauthorId.
Constraints / indexes: Indexes on (companyId, createdAt) and (leadId, createdAt).
A note is always anchored to a company; if its lead is deleted the note survives (
leadIdnulled). Notes haveeditedAtinstead ofupdatedAt— explicit edit tracking.
Tag
Purpose: The catalog/definition of a tag that can be applied to leads. Tags are categorized (TagCategory) — dispositions, ICP markers, gates, or custom. System tags (isSystem) are protected from deletion.
Table: tags
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
slug | String | no | — | slug |
name | String | no | — | name |
category | TagCategory | no | — | category |
color | String | yes | — | color |
description | String | yes | — | description |
isSystem | Boolean | no | false | is_system |
isActive | Boolean | no | true | is_active |
sortOrder | Int | no | 0 | sort_order |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: None (referenced by slug, not FK — see LeadTag.tagSlug).
Constraints / indexes: slug is @unique. Index on (category, isActive, sortOrder).
LeadTag
Purpose: The application of a tag to a specific lead (join record between Lead and Tag). Records who applied it and when, and supports soft-removal via removedAt. Applying certain disposition tags is what triggers automation sequences.
Table: lead_tags
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
leadId | String | no | — | lead_id |
tagSlug | String | no | — | tag_slug |
appliedAt | DateTime | no | now() | applied_at |
appliedBy | String | no | "nick" | applied_by |
removedAt | DateTime | yes | — | removed_at |
Relationships: lead → Lead via leadId.
Constraints / indexes: @@unique([leadId, tagSlug]) named unique_active_tag. Index on tagSlug.
tagSlugis a loose string reference toTag.slug(no enforced FK).appliedBydefaults to the string"nick"— a single-tenant default.
LeadScore
Purpose: Computed scoring/health metrics for a lead (1:1 with Lead). Holds quality score, engagement score, deal-risk classification, and recency-of-contact metrics.
Table: lead_scores
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
leadId | String | no | — | lead_id |
qualityScore | Int | no | 0 | quality_score |
engagementScore | Int | no | 0 | engagement_score |
dealRisk | DealRisk | no | UNKNOWN | deal_risk |
lastInteractionAt | DateTime | yes | — | last_interaction_at |
daysSinceContact | Int | yes | — | days_since_contact |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: lead → Lead via leadId.
Constraints / indexes: leadId is @unique (enforces 1:1).
TaxonomyValue
Purpose: The data-driven dropdown system. One row per selectable option for a given TaxonomyType (industry, source, vendor, cellular carrier, backend, payment interval, shipping type, payment method). Lets the operator manage list values without schema migrations. Consuming records store the chosen string value (e.g. Company.industry).
Table: taxonomy_values
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
type | TaxonomyType | no | — | type |
value | String | no | — | value |
sortOrder | Int | no | 0 | sort_order |
active | Boolean | no | true | active |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: None.
Constraints / indexes: @@unique([type, value]). Index on (type, active, sortOrder).
CompanyAddress
Purpose: A physical address belonging to a company — billing, shipping, or both. A company can have multiple addresses with default-billing / default-shipping flags. Invoices reference these for bill-to / ship-to.
Table: company_addresses
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
companyId | String | no | — | company_id |
label | String | no | — | label |
type | AddressType | no | — | type |
addressLine1 | String | no | — | address_line1 |
addressLine2 | String | yes | — | address_line2 |
city | String | no | — | city |
state | String | no | — | state |
zip | String | no | — | zip |
country | String | no | "US" | country |
notes | String | yes | — | notes |
isDefaultBilling | Boolean | no | false | is_default_billing |
isDefaultShipping | Boolean | no | false | is_default_shipping |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
company→CompanyviacompanyId, onDelete: Cascade.billedInvoices→Invoice[]via relation"InvoiceBillTo".shippedInvoices→Invoice[]via relation"InvoiceShipTo".
Constraints / indexes: Index on companyId.
Tasks
Task
Purpose: A to-do / follow-up item. Can be attached to a lead and/or a company, assigned to a user, scheduled (dueAt, optional Google Calendar event link), and optionally generated by a sequence step.
Table: tasks
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
leadId | String | yes | — | lead_id |
companyId | String | yes | — | company_id |
assignedToId | String | yes | — | assigned_to_id |
title | String | no | — | title |
description | String | yes | — | description |
taskType | TaskType | no | GENERAL | task_type |
priority | TaskPriority | no | NORMAL | priority |
dueAt | DateTime | yes | — | due_at |
calendarEventId | String | yes | — | calendar_event_id |
durationMin | Int | yes | — | duration_min |
completedAt | DateTime | yes | — | completed_at |
isCompleted | Boolean | no | false | is_completed |
sequenceRunId | String | yes | — | sequence_run_id |
sequenceStepNumber | Int | yes | — | sequence_step_number |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
lead→Lead?vialeadId.company→Company?viacompanyId.assignedTo→User?viaassignedToId.sequenceRun→SequenceRun?viasequenceRunId.
Constraints / indexes: @@unique([sequenceRunId, sequenceStepNumber]) named task_run_step_unique (prevents a sequence step generating its task twice). Indexes on leadId, (isCompleted, dueAt), priority.
Activity log
Activity
Purpose: The immutable audit trail of everything that happens in the CRM — stage changes, sends, payments, system events. Append-only: rows are written, never updated (note the absence of updatedAt). userId is nullable specifically to allow attribution to non-human / system actors (background jobs, the sequence worker, webhooks, AI agents). All subject FKs are nullable so an activity can be scoped to any combination of lead/company/contact.
Table: activities
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
leadId | String | yes | — | lead_id |
companyId | String | yes | — | company_id |
contactId | String | yes | — | contact_id |
userId | String | yes | — | user_id |
activityType | String | no | — | activity_type |
summary | String | no | — | summary |
detail | Json | yes | — | detail |
occurredAt | DateTime | no | now() | occurred_at |
createdAt | DateTime | no | now() | created_at |
Relationships:
lead→Lead?vialeadId.company→Company?viacompanyId.contact→Contact?viacontactId.user→User?viauserId.
Constraints / indexes: Indexes on (leadId, occurredAt), companyId, activityType.
activityTypeis a free-form string (not an enum) — kept open so new event kinds can be logged without a migration.detailis JSON for arbitrary structured payload.occurredAt(when the event happened) is distinct fromcreatedAt(when the row was written) to support backfilled / imported events.
Communications
EmailAccount
Purpose: A connected mailbox the CRM sends/receives email through. Primarily Gmail via OAuth (refresh/access tokens); SMTP fields are present as a fallback. Optionally owned by a User (supports the coming per-user Gmail routing).
Table: email_accounts
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
userId | String | yes | — | user_id |
emailAddress | String | no | — | email_address |
displayName | String | yes | — | display_name |
provider | String | no | "gmail" | provider |
gmailRefreshToken | String | yes | — | gmail_refresh_token |
gmailAccessToken | String | yes | — | gmail_access_token |
gmailTokenExpiry | DateTime | yes | — | gmail_token_expiry |
smtpHost | String | yes | — | smtp_host |
smtpPort | Int | yes | — | smtp_port |
smtpUser | String | yes | — | smtp_user |
smtpPass | String | yes | — | smtp_pass |
isActive | Boolean | no | true | is_active |
lastSyncedAt | DateTime | yes | — | last_synced_at |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
user→User?viauserId, onDelete: SetNull.threads→EmailThread[].
Constraints / indexes: None beyond PK.
EmailThread
Purpose: A conversation thread of emails, tied to a Gmail thread and an EmailAccount, optionally associated with a Lead.
Table: email_threads
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
leadId | String | yes | — | lead_id |
emailAccountId | String | no | — | email_account_id |
gmailThreadId | String | yes | — | gmail_thread_id |
subject | String | yes | — | subject |
lastMessageAt | DateTime | yes | — | last_message_at |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
lead→Lead?vialeadId.emailAccount→EmailAccountviaemailAccountId.messages→EmailMessage[].
Constraints / indexes: Indexes on leadId, gmailThreadId.
EmailMessage
Purpose: A single email within a thread (inbound or outbound), with rendered text/HTML bodies.
Table: email_messages
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
threadId | String | no | — | thread_id |
gmailMessageId | String | yes | — | gmail_message_id |
direction | MessageDirection | no | — | direction |
fromAddress | String | no | — | from_address |
toAddress | String | no | — | to_address |
subject | String | yes | — | subject |
bodyText | String | yes | — | body_text |
bodyHtml | String | yes | — | body_html |
sentAt | DateTime | yes | — | sent_at |
createdAt | DateTime | no | now() | created_at |
Relationships: thread → EmailThread via threadId.
Constraints / indexes: Index on threadId.
SmsGateway
Purpose: An outbound SMS channel — an Android-phone SMS gateway. Holds gateway URL, credentials (encrypted password), and the sending phone number (raw and E.164). Optionally owned by a User (per-user SMS gateways are on the roadmap).
Table: sms_gateways
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
userId | String | yes | — | user_id |
label | String | no | — | label |
gatewayUrl | String | no | — | gateway_url |
username | String | no | — | username |
passwordEncrypted | String | no | — | password_encrypted |
fromNumber | String | no | — | from_number |
fromNumberE164 | String | no | — | from_number_e164 |
isActive | Boolean | no | true | is_active |
lastSeenAt | DateTime | yes | — | last_seen_at |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: user → User? via userId, onDelete: SetNull.
Constraints / indexes: Index on fromNumberE164.
SmsMessage
Purpose: A single SMS message, inbound or outbound, optionally tied to a lead and/or contact. Tracks delivery status and provider message ID.
Table: sms_messages
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
leadId | String | yes | — | lead_id |
contactId | String | yes | — | contact_id |
direction | MessageDirection | no | — | direction |
fromNumber | String | no | — | from_number |
toNumber | String | no | — | to_number |
body | String | no | — | body |
provider | String | no | "android_gateway" | provider |
providerMessageId | String | yes | — | provider_message_id |
status | SmsStatus | no | QUEUED | status |
sentAt | DateTime | yes | — | sent_at |
createdAt | DateTime | no | now() | created_at |
Relationships:
lead→Lead?vialeadId.contact→Contact?viacontactId.
Constraints / indexes: Indexes on leadId, contactId.
CallLog
Purpose: A record of a phone call — inbound or outbound — with outcome, duration, notes, optional recording URL and transcript, and a voicemail-drop flag.
Table: call_logs
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
leadId | String | yes | — | lead_id |
contactId | String | yes | — | contact_id |
direction | MessageDirection | no | — | direction |
durationSeconds | Int | yes | — | duration_seconds |
outcome | CallOutcome | no | — | outcome |
notes | String | yes | — | notes |
recordingUrl | String | yes | — | recording_url |
transcript | String | yes | — | transcript |
voicemailDropped | Boolean | no | false | voicemail_dropped |
provider | String | no | "manual" | provider |
createdAt | DateTime | no | now() | created_at |
Relationships:
lead→Lead?vialeadId.contact→Contact?viacontactId.
Constraints / indexes: Index on leadId.
Conversation
Purpose: A unified-inbox thread — one per lead per channel (SMS / EMAIL / CALL). Surfaces the inbox UI: last-message preview, unread count, archive state. Optionally assigned to a User.
Table: conversations
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
leadId | String | yes | — | lead_id |
contactId | String | yes | — | contact_id |
userId | String | yes | — | user_id |
channel | ConversationChannel | no | — | channel |
lastMessageAt | DateTime | yes | — | last_message_at |
lastMessagePreview | String | yes | — | last_message_preview |
unreadCount | Int | no | 0 | unread_count |
isArchived | Boolean | no | false | is_archived |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
lead→Lead?vialeadId.contact→Contact?viacontactId.user→User?viauserId, onDelete: SetNull.
Constraints / indexes: @@unique([leadId, channel]) (one conversation per lead per channel). Indexes on lastMessageAt (descending), isArchived, userId.
Booking
Purpose: A scheduled meeting, synced from Cal.com (self-hosted at book.trygpsleaders.com). Stores the Cal booking UID, attendee, event type, time window, status, and meeting/reschedule/cancel URLs, plus the raw webhook payload.
Table: bookings
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
calBookingUid | String | no | — | cal_booking_uid |
leadId | String | yes | — | lead_id |
contactId | String | yes | — | contact_id |
attendeeEmail | String | no | — | attendee_email |
attendeeName | String | yes | — | attendee_name |
eventTypeSlug | String | no | — | event_type_slug |
eventTypeTitle | String | no | — | event_type_title |
startAt | DateTime | no | — | start_at |
endAt | DateTime | no | — | end_at |
status | BookingStatus | no | SCHEDULED | status |
meetingUrl | String | yes | — | meeting_url |
rescheduleUrl | String | yes | — | reschedule_url |
cancelUrl | String | yes | — | cancel_url |
cancelledAt | DateTime | yes | — | cancelled_at |
cancelReason | String | yes | — | cancel_reason |
rawPayload | Json | yes | — | raw_payload |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
lead→Lead?vialeadId, onDelete: SetNull.contact→Contact?viacontactId, onDelete: SetNull.
Constraints / indexes: calBookingUid is @unique. Indexes on (leadId, startAt), contactId, attendeeEmail, startAt.
CallList
Purpose: A saved power-dialer call list — a named, ordered set of leads to call through. Stores a snapshot of lead IDs and optionally the filter criteria used to build it.
Table: call_lists
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
name | String | no | — | name |
filterCriteria | Json | yes | — | filter_criteria |
leadIds | String[] | no | — (array) | lead_ids |
position | Int | no | 0 | position |
isActive | Boolean | no | true | is_active |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: None (leadIds is a plain string array, not an FK relation).
Constraints / indexes: None beyond PK.
VoicemailDrop
Purpose: A pre-recorded voicemail audio clip that can be "dropped" during the power-dialer flow.
Table: voicemail_drops
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
name | String | no | — | name |
audioUrl | String | no | — | audio_url |
durationSeconds | Int | no | — | duration_seconds |
isDefault | Boolean | no | false | is_default |
createdAt | DateTime | no | now() | created_at |
Relationships: None.
Constraints / indexes: None beyond PK.
Sequences
Disposition-triggered automation: applying a disposition tag to a lead starts a SequenceRun of the matching SequenceDefinition; a background worker advances runs step by step.
SequenceDefinition
Purpose: The template for an automation sequence. Keyed by tagSlug — the disposition tag that triggers it. The ordered steps are stored as a JSON array.
Table: sequence_definitions
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
tagSlug | String | no | — | tag_slug |
name | String | no | — | name |
description | String | yes | — | description |
steps | Json | no | — | steps |
isActive | Boolean | no | true | is_active |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: runs → SequenceRun[].
Constraints / indexes: tagSlug is @unique (one definition per trigger tag).
SequenceRun
Purpose: A live instance of a sequence executing against one lead. Tracks current step, the worker's next-fire time (nextStepAt), and lifecycle state (active / paused / completed / canceled) with reasons.
Table: sequence_runs
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
leadId | String | no | — | lead_id |
sequenceDefId | String | no | — | sequence_def_id |
tagSlug | String | no | — | tag_slug |
status | SequenceStatus | no | ACTIVE | status |
currentStep | Int | no | 0 | current_step |
startedAt | DateTime | no | now() | started_at |
nextStepAt | DateTime | yes | — | next_step_at |
completedAt | DateTime | yes | — | completed_at |
pausedAt | DateTime | yes | — | paused_at |
pausedReason | String | yes | — | paused_reason |
canceledAt | DateTime | yes | — | canceled_at |
canceledReason | String | yes | — | canceled_reason |
createdAt | DateTime | no | now() | created_at |
Relationships:
lead→LeadvialeadId.sequenceDef→SequenceDefinitionviasequenceDefId.stepLogs→SequenceStepLog[].tasks→Task[].
Constraints / indexes: Indexes on (leadId, status) and nextStepAt (the worker polls by nextStepAt).
SequenceStepLog
Purpose: A log of each step within a run — when it was scheduled, when it fired, and any error. Provides the per-step audit/diagnostics for the sequence engine.
Table: sequence_step_logs
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
sequenceRunId | String | no | — | sequence_run_id |
stepNumber | Int | no | — | step_number |
actionType | String | no | — | action_type |
status | StepStatus | no | PENDING | status |
scheduledAt | DateTime | yes | — | scheduled_at |
firedAt | DateTime | yes | — | fired_at |
errorMessage | String | yes | — | error_message |
createdAt | DateTime | no | now() | created_at |
Relationships: sequenceRun → SequenceRun via sequenceRunId.
Constraints / indexes: Index on sequenceRunId.
Financial
Invoicing with QuickBooks (qb*Id fields) and Stripe (stripe*Id fields) synchronization. Invoices reference several dedicated relational reference tables.
Invoice
Purpose: A customer invoice. Always belongs to a Company (the master record); optionally tied to a Lead. Carries pricing totals, e-signature capture for the agreement, a public payment token, status lifecycle, and external IDs for Stripe and QuickBooks.
Table: invoices
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
leadId | String | yes | — | lead_id |
companyId | String | no | — | company_id |
billToAddressId | String | yes | — | bill_to_address_id |
shipToAddressId | String | yes | — | ship_to_address_id |
invoiceNumber | String | no | — | invoice_number |
status | InvoiceStatus | no | DRAFT | status |
agreementTemplateId | String | yes | — | agreement_template_id |
taxRateId | String | yes | — | tax_rate_id |
backendId | String | yes | — | backend_id |
carrierId | String | yes | — | carrier_id |
shippingTypeId | String | yes | — | shipping_type_id |
paymentTermId | String | yes | — | payment_term_id |
contractTermId | String | yes | — | contract_term_id |
subtotal | Decimal(10,2) | no | — | subtotal |
taxAmount | Decimal(10,2) | no | 0 | tax_amount |
total | Decimal(10,2) | no | — | total |
amountPaid | Decimal(10,2) | no | 0 | amount_paid |
balanceDue | Decimal(10,2) | no | — | balance_due |
shippingCost | Decimal(10,2) | yes | — | shipping_cost |
dueDate | DateTime @db.Date | yes | — | due_date |
notes | String | yes | — | notes |
paymentToken | String | yes | — | payment_token |
stripeInvoiceId | String | yes | — | stripe_invoice_id |
qbInvoiceId | String | yes | — | qb_invoice_id |
sentAt | DateTime | yes | — | sent_at |
paidAt | DateTime | yes | — | paid_at |
voidedAt | DateTime | yes | — | voided_at |
signedName | String | yes | — | signed_name |
signedAt | DateTime | yes | — | signed_at |
signedIpAddr | String | yes | — | signed_ip_addr |
signedUserAgent | String | yes | — | signed_user_agent |
signedAgreementSnapshot | String @db.Text | yes | — | signed_agreement_snapshot |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
lead→Lead?vialeadId.company→CompanyviacompanyId(required).billToAddress→CompanyAddress?relation"InvoiceBillTo"viabillToAddressId, onDelete: SetNull.shipToAddress→CompanyAddress?relation"InvoiceShipTo"viashipToAddressId, onDelete: SetNull.agreementTemplate→AgreementTemplate?viaagreementTemplateId.taxRate→TaxRate?viataxRateId.backend→Backend?viabackendId.carrier→Carrier?viacarrierId.shippingType→ShippingType?viashippingTypeId.paymentTerm→PaymentTerm?viapaymentTermId.contractTerm→ContractTerm?viacontractTermId.lineItems→InvoiceLineItem[].payments→Payment[].fulfillmentOrders→FulfillmentOrder[].devices→Device[].
Constraints / indexes: invoiceNumber is @unique; paymentToken is @unique. Indexes on companyId, billToAddressId, shipToAddressId, status, agreementTemplateId, taxRateId.
leadIdis frequentlynullin production — resolve an invoice's business context viacompanyId(the always-present master link) rather than relying on the lead. Thesigned*fields capture a legally-meaningful e-signature event (name, timestamp, IP, user agent, and a frozen text snapshot of the agreement at signing time).
InvoiceLineItem
Purpose: A single billable line on an invoice.
Table: invoice_line_items
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
invoiceId | String | no | — | invoice_id |
description | String | no | — | description |
quantity | Int | no | — | quantity |
unitPrice | Decimal(10,2) | no | — | unit_price |
total | Decimal(10,2) | no | — | total |
productCode | String | yes | — | product_code |
createdAt | DateTime | no | now() | created_at |
Relationships: invoice → Invoice via invoiceId, onDelete: Cascade.
Constraints / indexes: None beyond PK.
Payment
Purpose: A payment recorded against an invoice. Method-typed; supports Stripe-collected payments (stripePaymentId) and manually-entered payments (check/cash/wire/other). Carries an optional QuickBooks payment ID for sync.
Table: payments
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
invoiceId | String | no | — | invoice_id |
amount | Decimal(10,2) | no | — | amount |
method | PaymentMethod | no | — | method |
stripePaymentId | String | yes | — | stripe_payment_id |
reference | String | yes | — | reference |
notes | String | yes | — | notes |
paidAt | DateTime | no | — | paid_at |
qbPaymentId | String | yes | — | qb_payment_id |
createdAt | DateTime | no | now() | created_at |
Relationships: invoice → Invoice via invoiceId.
Constraints / indexes: stripePaymentId is @unique (idempotency for Stripe webhooks). Index on invoiceId.
Product
Purpose: A product/service catalog item, synced from QuickBooks (qbItemId is the link). Holds price, cost, SKU, category, and product type.
Table: products
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
qbItemId | String | no | — | qb_item_id |
name | String | no | — | name |
description | String | yes | — | description |
sku | String | yes | — | sku |
unitPrice | Decimal(10,2) | no | — | unit_price |
cost | Decimal(10,2) | yes | — | cost |
category | String | yes | — | category |
type | ProductType | no | ONE_TIME | type |
active | Boolean | no | true | active |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
lastSyncedAt | DateTime | yes | — | last_synced_at |
Relationships: None (referenced loosely via InvoiceLineItem.productCode).
Constraints / indexes: qbItemId is @unique. Indexes on active, sku, category.
AgreementTemplate
Purpose: A reusable terms-and-conditions / agreement body that gets attached to an invoice and snapshotted at signing. Supports {{variable}} substitution via the variables JSON.
Table: agreement_templates
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
slug | String | no | — | slug |
name | String | no | — | name |
body | String @db.Text | no | — | body |
variables | Json | no | — | variables |
active | Boolean | no | true | active |
sortOrder | Int | no | 0 | sort_order |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: invoices → Invoice[].
Constraints / indexes: slug is @unique. Index on (active, sortOrder).
TaxRate
Purpose: A named tax rate (CA district, resale, out-of-state, etc.) selectable on an invoice.
Table: tax_rates
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
slug | String | no | — | slug |
name | String | no | — | name |
rate | Decimal(7,5) | no | — | rate |
state | String | yes | — | state |
jurisdiction | String | yes | — | jurisdiction |
category | TaxRateCategory | no | — | category |
active | Boolean | no | true | active |
sortOrder | Int | no | 0 | sort_order |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: invoices → Invoice[].
Constraints / indexes: slug is @unique. Index on (category, active, sortOrder).
Backend
Purpose: Reference table of GPS "backend" platforms (the tracking software backend a device runs on) selectable on an invoice.
Table: backends
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
slug | String | no | — | slug |
name | String | no | — | name |
sortOrder | Int | no | 0 | sort_order |
active | Boolean | no | true | active |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: invoices → Invoice[].
Constraints / indexes: slug is @unique. Index on (active, sortOrder).
Carrier
Purpose: Reference table of cellular carriers selectable on an invoice (the SIM/airtime carrier for the GPS devices).
Table: carriers
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
slug | String | no | — | slug |
name | String | no | — | name |
sortOrder | Int | no | 0 | sort_order |
active | Boolean | no | true | active |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: invoices → Invoice[].
Constraints / indexes: slug is @unique. Index on (active, sortOrder).
ShippingType
Purpose: Reference table of shipping methods selectable on an invoice, each with an optional default cost.
Table: shipping_types
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
slug | String | no | — | slug |
name | String | no | — | name |
defaultCost | Decimal(10,2) | yes | — | default_cost |
sortOrder | Int | no | 0 | sort_order |
active | Boolean | no | true | active |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: invoices → Invoice[].
Constraints / indexes: slug is @unique. Index on (active, sortOrder).
PaymentTerm
Purpose: Reference table of payment terms (e.g. Net 30) selectable on an invoice.
Table: payment_terms
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
slug | String | no | — | slug |
name | String | no | — | name |
sortOrder | Int | no | 0 | sort_order |
active | Boolean | no | true | active |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: invoices → Invoice[].
Constraints / indexes: slug is @unique. Index on (active, sortOrder).
ContractTerm
Purpose: Reference table of contract-length terms selectable on an invoice, with an optional months value.
Table: contract_terms
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
slug | String | no | — | slug |
name | String | no | — | name |
months | Int | yes | — | months |
sortOrder | Int | no | 0 | sort_order |
active | Boolean | no | true | active |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: invoices → Invoice[].
Constraints / indexes: slug is @unique. Index on (active, sortOrder).
Inventory
GPS hardware tracking — physical devices and their SIM cards, plus the fulfillment workflow that ships them.
Device
Purpose: A physical GPS tracking device (default model GL300). Tracks lifecycle status, optional assignment to a company and an invoice, an optional 1:1 SIM card, and activation/tracking state.
Table: devices
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
serialNumber | String | no | — | serial_number |
imei | String | yes | — | imei |
model | String | no | "GL300" | model |
status | DeviceStatus | no | IN_STOCK | status |
companyId | String | yes | — | company_id |
invoiceId | String | yes | — | invoice_id |
simCardId | String | yes | — | sim_card_id |
activationDate | DateTime @db.Date | yes | — | activation_date |
trackingActive | Boolean | no | false | tracking_active |
notes | String | yes | — | notes |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
company→Company?viacompanyId.invoice→Invoice?viainvoiceId.simCard→SimCard?viasimCardId(1:1).fulfillmentItems→FulfillmentItem[].
Constraints / indexes: serialNumber is @unique; imei is @unique; simCardId is @unique (enforces 1:1 with SimCard). Indexes on status, companyId.
SimCard
Purpose: A cellular SIM card that pairs 1:1 with a Device. Tracks ICCID, phone number, carrier, and lifecycle status.
Table: sim_cards
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
iccid | String | no | — | iccid |
phoneNumber | String | yes | — | phone_number |
carrier | String | yes | — | carrier |
status | SimStatus | no | IN_STOCK | status |
activatedAt | DateTime | yes | — | activated_at |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
device→Device?(back-relation ofDevice.simCard; 1:1).fulfillmentItems→FulfillmentItem[].
Constraints / indexes: iccid is @unique. Index on status.
FulfillmentOrder
Purpose: A shipment of devices fulfilling an invoice. Tracks status through the warehouse workflow, device count, carrier, tracking number, and ship/deliver timestamps.
Table: fulfillment_orders
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
invoiceId | String | no | — | invoice_id |
companyId | String | no | — | company_id |
status | FulfillmentStatus | no | PENDING | status |
deviceCount | Int | no | — | device_count |
trackingNumber | String | yes | — | tracking_number |
carrier | String | yes | — | carrier |
shippedAt | DateTime | yes | — | shipped_at |
deliveredAt | DateTime | yes | — | delivered_at |
notes | String | yes | — | notes |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships:
invoice→InvoiceviainvoiceId.company→CompanyviacompanyId.items→FulfillmentItem[].
Constraints / indexes: Indexes on status, invoiceId.
FulfillmentItem
Purpose: A line within a fulfillment order — one specific device (and optionally its SIM card) being shipped.
Table: fulfillment_items
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
fulfillmentOrderId | String | no | — | fulfillment_order_id |
deviceId | String | no | — | device_id |
simCardId | String | yes | — | sim_card_id |
createdAt | DateTime | no | now() | created_at |
Relationships:
fulfillmentOrder→FulfillmentOrderviafulfillmentOrderId, onDelete: Cascade.device→DeviceviadeviceId.simCard→SimCard?viasimCardId.
Constraints / indexes: None beyond PK.
Settings / Templates
Setting
Purpose: A generic key/value application settings store. One row per setting key; value is arbitrary JSON.
Table: settings
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
key | String | no | — | key |
value | Json | no | — | value |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: None.
Constraints / indexes: key is @unique. (Note: this model has no createdAt.)
MessageTemplate
Purpose: A reusable email or SMS message template. Used both as sequence content and as canned quick-replies (TemplateCategory). Optionally attributed to the user who created it.
Table: message_templates
| Field | Type | Null | Default | Column |
|---|---|---|---|---|
id | String | no | uuid() | id |
name | String | no | — | name |
slug | String | no | — | slug |
channel | TemplateChannel | no | — | channel |
category | TemplateCategory | no | SEQUENCE | category |
subject | String | yes | — | subject |
body | String | no | — | body |
isActive | Boolean | no | true | is_active |
createdById | String | yes | — | created_by_id |
createdAt | DateTime | no | now() | created_at |
updatedAt | DateTime | no | @updatedAt | updated_at |
Relationships: createdBy → User? via createdById.
Constraints / indexes: slug is @unique. Index on (category, channel, isActive).
Enum Reference
All 26 enums and their complete value sets.
UserRole
Role of a user/actor. Includes AI_AGENT so a "user" can be a non-human seat.
ADMIN, REP, OPERATOR, NICK, AI_AGENT
CompanyStatus
What a company is — drives classification of the master record.
LEAD, CUSTOMER, PARTNER, CONTACT, INACTIVE, DEAD
AddressType
Role of a company address.
BILLING, SHIPPING, BOTH
TaxonomyType
Which data-driven dropdown list a TaxonomyValue belongs to.
INDUSTRY, SOURCE, VENDOR, CELLULAR_CARRIER, BACKEND, PAYMENT_INTERVAL, SHIPPING_TYPE, PAYMENT_METHOD
TagCategory
Classification of a Tag.
DISPOSITION, ICP, GATE, CUSTOM
LeadStage
Pipeline stage of a Lead.
NEW_LEAD, HOT, QUALIFIED, QUOTED, BUY_PERIOD_30, BUY_PERIOD_60, BUY_PERIOD_90, WON, LOST, BOGUS
DealRisk
Deal-risk classification on a LeadScore.
LOW, MEDIUM, HIGH, UNKNOWN
TaskType
Kind of a Task.
CALL, EMAIL, SMS, FOLLOW_UP, GENERAL
TaskPriority
Priority of a Task.
URGENT, HIGH, NORMAL, LOW
BookingStatus
State of a Booking.
SCHEDULED, RESCHEDULED, CANCELLED
MessageDirection
Direction of a communication (email / SMS / call).
INBOUND, OUTBOUND
SmsStatus
Delivery status of an SmsMessage.
QUEUED, SENT, DELIVERED, FAILED
CallOutcome
Result of a logged call.
CONNECTED, NO_ANSWER, VOICEMAIL, BUSY, WRONG_NUMBER
ConversationChannel
Channel of a unified-inbox Conversation.
SMS, EMAIL, CALL
SequenceStatus
Lifecycle state of a SequenceRun.
ACTIVE, PAUSED, COMPLETED, CANCELED
StepStatus
State of a SequenceStepLog entry.
PENDING, FIRED, SKIPPED, FAILED
ProductType
Billing type of a Product.
ONE_TIME, RECURRING, SERVICE
TaxRateCategory
Category of a TaxRate.
CA_DISTRICT, RESALE, OUT_OF_STATE, OTHER_STATE, OTHER
InvoiceStatus
Lifecycle state of an Invoice.
DRAFT, SENT, PARTIAL, PAID, VOID
PaymentMethod
How a Payment was made.
STRIPE, CHECK, CASH, WIRE, OTHER
DeviceStatus
Lifecycle state of a GPS Device.
IN_STOCK, ASSIGNED, SHIPPED, ACTIVATED, RETURNED, DEFECTIVE
SimStatus
Lifecycle state of a SimCard.
IN_STOCK, ASSIGNED, ACTIVE, DEACTIVATED
FulfillmentStatus
State of a FulfillmentOrder through the warehouse workflow.
PENDING, PICKING, PACKED, SHIPPED, DELIVERED, CANCELED
TemplateChannel
Channel of a MessageTemplate.
EMAIL, SMS
TemplateCategory
Use of a MessageTemplate.
SEQUENCE, CANNED
Cross-Cutting Notes
- External system sync. QuickBooks links:
Company.qbCustomerId,Invoice.qbInvoiceId,Payment.qbPaymentId,Product.qbItemId(unique — the catalog key). Stripe links:Company.stripeCustomerId,Invoice.stripeInvoiceId,Payment.stripePaymentId(unique — webhook idempotency). - Soft vs hard delete. True
Cascadedeletes exist only on tightly-owned children:CompanyAddress,Note,InvoiceLineItem,FulfillmentItem.SetNullis used to preserve history when a parent is removed (Note.lead,Invoicebill/ship addresses,Bookinglead/contact, anduserIdback-references). Many models use soft-delete / soft-removal flags instead (isActive,active,LeadTag.removedAt,Conversation.isArchived). onDeletedefaults. Where noonDeleteis specified on a required relation, Prisma defaults toRestrict/NoAction; on optional relations it defaults toSetNull. Most lead/company FKs on communication and task records are optional with no explicit clause.- Decimal precision. Money fields use
Decimal(10,2);Company.googleRatingusesDecimal(2,1);TaxRate.rateusesDecimal(7,5)for fine-grained tax percentages. @db.Datefields.Invoice.dueDateandDevice.activationDateare date-only (no time component).- JSON fields.
Activity.detail,Booking.rawPayload,CallList.filterCriteria,SequenceDefinition.steps,AgreementTemplate.variables,Setting.valueare allJson. - Roadmap-aware design.
EmailAccount.userIdandSmsGateway.userIdare nullable and per-user — built for the coming multi-account routing (per-user Gmail accounts + per-user Android SMS gateways).UserRole.AI_AGENTand the absence of a hard single-ADMIN constraint keep the door open for the org/agent-fleet direction.