Skip to main content

Database Design - WhatsApp SaaS Platform

Overview

This schema supports a multi-tenant WhatsApp SaaS platform with vendor-based isolation, shared and dedicated WABA accounts, message routing, campaigns, billing, templates, webhooks, and API audit logging.

Architecture Principles

AreaDesign
Tenant rootVendor
Tenant isolationvendorId on tenant-owned models
Shared WABAWabaAccount.isShared = true, selected at runtime
Dedicated WABAVendor.dedicatedWabaId, enforced by application logic
Routing identityRequired MessageLog.phoneNumberId
Analytics traceOptional direct MessageLog.wabaId
Recipient auditRequired MessageLog.recipientMobile
Idempotency@@unique([vendorId, idempotencyKey])
Queue readinessstatus, priority, retryCount, nextRetryAt, expiresAt
Migration ruleprisma migrate deploy in staging/prod. Never db push.
Staging DBkloyst_staging — fully isolated from production

Core Models

Vendor

Vendor is the tenant root. Users, contacts, campaigns, messages, wallet, templates, webhooks, and API logs are scoped from it.

FieldTypeNotes
idUUIDPrimary key
nameVARCHAR(255)Tenant name
planTypePlanTypeSHARED or DEDICATED
settingsJson?Flexible tenant settings
dedicatedWabaIdUUID?Optional dedicated WABA reference
createdAt / updatedAtDateTimeAudit timestamps

WabaAccount

WabaAccount is the unified WABA model for both shared and dedicated Meta WhatsApp Business Accounts.

FieldTypeNotes
idUUIDPrimary key
wabaIdVARCHAR(255)Unique Meta WABA ID
isSharedBooleanShared vs dedicated account marker
businessNameVARCHAR(255)?Meta business name
systemUserTokenString?Meta API credential
qualityRatingVARCHAR(50)?Meta quality rating
createdAt / updatedAtDateTimeAudit timestamps

Dedicated WABA exclusivity is intentionally not enforced with a database unique constraint on Vendor.dedicatedWabaId. The application layer validates assignment rules.

PhoneNumber

PhoneNumber remains separate because a WABA can have many sender numbers and messages must always preserve the sender/routing number.

FieldTypeNotes
idUUIDPrimary key
phoneNumberIdVARCHAR(255)Unique Meta phone number ID
displayNumberVARCHAR(20)Visible phone number
verifiedNameVARCHAR(255)?WhatsApp verified name
qualityRatingVARCHAR(50)?Meta quality rating
statusVARCHAR(50)?Meta number status
wabaIdUUIDParent WABA
createdAt / updatedAtDateTimeAudit timestamps

MessageLog

MessageLog is the high-volume audit and queue table. It is directly tenant-scoped by vendorId.

FieldTypeNotes
idUUIDPrimary key
waMessageIdVARCHAR(255)?Unique Meta message ID
statusMessageStatusQUEUED, SENT, DELIVERED, READ, FAILED
directionMessageDirectionINBOUND or OUTBOUND
errorString?Failure details
payloadJson?Debug request/response payload
pricingJson?Billing data
contentString?Message content
messageTypeVARCHAR(50)?text, image, document, etc.
recipientMobileVARCHAR(20)Immutable recipient identity
sourceVARCHAR(50)?API, CAMPAIGN, WEBHOOK, RETRY, etc.
priorityIntQueue priority
sentAt / deliveredAt / readAt / failedAtDateTime?Lifecycle timestamps
retryCountIntRetry attempts
nextRetryAtDateTime?Retry worker schedule
expiresAtDateTime?Queue/message expiry
idempotencyKeyVARCHAR(255)?Per-vendor idempotency key
vendorIdUUIDTenant isolation
phoneNumberIdUUIDRequired routing identity
wabaIdUUID?Direct WABA trace
contactIdUUID?Optional contact reference
campaignIdUUID?Optional campaign reference

Campaign (Updated)

Campaigns now include cost estimation linkage and template variable support.

FieldTypeNotes
idUUIDPrimary key
nameVARCHAR(255)Campaign name
statusCampaignStatusDRAFT, QUEUED, PROCESSING, COMPLETED, FAILED
scheduledAtDateTime?Optional send schedule
templateIdUUID?Linked WhatsApp template (SetNull on delete)
templateVariablesJson?Variable values for the template
audienceTypeVARCHAR(50)?LABEL, CONTACT_LIST, FILTER_QUERY, SAVED_AUDIENCE
audienceIdsJson?Selected audience IDs
totalRecipientsIntTotal count before validation
validRecipientsIntCount after dedup + validation
estimatedCostDecimal(12,4)?Pre-send cost estimate
quotationIdUUID?Linked quotation document (SetNull on delete)
vendorIdUUIDTenant isolation

Pricing Engine Models

The pricing engine models support per-country rate cards, volume discounts, platform fees, and immutable quotation snapshots.

ModelPurpose
PricingRegionCountry code → region group mapping (e.g., IN → South Asia)
PricingRateBase rate per message per TemplateCategory per region group
PricingTierVolume-based discount tiers (discountPct applied to base rate)
PlatformFeeRuleKloyst markup: PERCENTAGE, FLAT, or HYBRID; per-vendor overrides
PricingSnapshotImmutable JSON snapshot of rates + fees at quote time
QuotationPre-campaign cost estimate with PDF generation lifecycle
QuotationItemPer-country breakdown row within a Quotation

Quotation

FieldTypeNotes
idUUIDPrimary key
quoteNumberVARCHAR(50)Human-readable unique ref (e.g. KQ-00042)
vendorIdUUIDTenant isolation
statusQuotationStatusDRAFT, ISSUED, EXPIRED, SUPERSEDED
categoryTemplateCategoryCampaign message category
totalRecipientsIntFull list size
validRecipientsIntAfter dedup and validation
estimatedMetaCostDecimal(12,4)Sum of Meta charges
platformFeeDecimal(12,4)Kloyst markup
estimatedTotalDecimal(12,4)Meta + platform fee
walletBalanceDecimal(12,4)?Balance at time of quote
walletSufficientBoolean?Whether wallet covers total
pdfStatusPdfStatusPENDING → GENERATING → READY → FAILED
pdfPathString?File path to generated PDF
validUntilDateTimeQuote expiry timestamp
snapshotIdUUIDLinked PricingSnapshot (immutable)

Labels & ContactAuditLog

Label

Labels allow vendors to tag contacts for audience targeting.

FieldTypeNotes
idUUIDPrimary key
vendorIdUUIDTenant isolation
nameVARCHAR(255)Label name
colorVARCHAR(50)?UI display color

Unique: [vendorId, name] — label names are unique per vendor.

ContactAuditLog

Immutable history of contact data mutations (name changes, label assignments) per import job.

FieldTypeNotes
idUUIDPrimary key
vendorIdUUIDTenant isolation
contactIdUUIDContact that was modified
actionVARCHAR(50)CREATED, METADATA_UPDATED, LABELS_MERGED
sourceContactSourceIMPORT, MANUAL, API, SYNC
importJobIdUUID?Linked import job
previousNameVARCHAR(255)?Before update
newNameVARCHAR(255)?After update
previousLabelsJson?Before label state
newLabelsJson?After label state

Auth Models

RefreshToken

Server-side refresh token storage (hashed). Enables token rotation and per-device revocation.

FieldTypeNotes
idUUIDPrimary key
userIdUUIDOwning user (Cascade delete)
tokenHashVARCHAR(255)Unique HMAC hash of token value
userAgentString?Browser/device info
ipAddressString?Client IP at issue time
expiresAtDateTimeToken expiry

VerificationToken

One-time tokens for email verification and password reset.

FieldTypeNotes
idUUIDPrimary key
tokenHashVARCHAR(255)Unique hash (never store plaintext)
userIdUUIDOwning user (Cascade delete)
typeTokenTypeEMAIL_VERIFY or PASSWORD_RESET
expiresAtDateTimeExpiry
usedAtDateTime?Set when consumed (prevents reuse)

WABA Architecture

Wallet is one-to-one with Vendor. Transaction is scoped through walletId.

ModelPurpose
WalletStores current balance with Decimal(12,4)
TransactionStores credit, debit, and refund ledger events

Templates

Template supports both vendor-level and WABA-level templates.

ScopeRepresentation
Vendor-levelvendorId set, wabaId = null
WABA-levelvendorId set, wabaId points to WabaAccount

Webhooks and ApiLog

Webhook stores tenant event subscriptions. ApiLog stores API audit and analytics events.

WABA Architecture

Message Lifecycle

Queue Processing

Workers can select messages with:

  • status
  • priority
  • nextRetryAt
  • expiresAt
  • vendorId
  • phoneNumberId

The supporting index is:

@@index([status, priority, nextRetryAt])

Security and Isolation

  • All tenant-owned reads must include vendorId.
  • Shared WABA data is never treated as tenant-owned.
  • Message isolation is enforced through MessageLog.vendorId.
  • Contact deletion does not erase recipient identity because recipientMobile is immutable on MessageLog.