Skip to content

Database Schema

ORM: Drizzle ORM Databases: PostgreSQL (primary), MongoDB (whatsapp-web-server sessions) Generated: 2026-01-04

NexisChat uses a multi-database architecture:

  • PostgreSQL (via Drizzle): User accounts, subscriptions, folders, templates
  • MongoDB (via Mongoose): WhatsApp session data (managed by whatsapp-web-js)

Core user identity (Better Auth managed).

ColumnTypeConstraintsDescription
idvarchar(36)PRIMARY KEYUser UUID
nametextNOT NULLDisplay name
emailtextUNIQUE, NOT NULLEmail address
emailVerifiedbooleanNOT NULLEmail verification status
imagetextProfile image URL
createdAttimestampNOT NULLCreation timestamp
updatedAttimestampNOT NULLLast update timestamp

User sessions (Better Auth managed).

ColumnTypeConstraintsDescription
idvarchar(36)PRIMARY KEYSession UUID
expiresAttimestampNOT NULLSession expiry
tokentextUNIQUE, NOT NULLSession token
createdAttimestampNOT NULLCreation timestamp
updatedAttimestampNOT NULLLast update
ipAddresstextClient IP
userAgenttextBrowser user agent
userIdvarchar(36)FK → user.idOwner user

OAuth provider connections (Better Auth managed).

ColumnTypeConstraintsDescription
idvarchar(36)PRIMARY KEYAccount UUID
accountIdtextNOT NULLProvider account ID
providerIdtextNOT NULLOAuth provider name
userIdvarchar(36)FK → user.idOwner user
accessTokentextOAuth access token
refreshTokentextOAuth refresh token
idTokentextOIDC ID token
accessTokenExpiresAttimestampToken expiry
refreshTokenExpiresAttimestampRefresh expiry
scopetextOAuth scopes
passwordtextHashed password (credentials)
createdAttimestampNOT NULLCreation timestamp
updatedAttimestampNOT NULLLast update

Email/phone verification tokens (Better Auth managed).

ColumnTypeConstraintsDescription
idvarchar(36)PRIMARY KEYVerification UUID
identifiertextNOT NULLEmail/phone to verify
valuetextNOT NULLVerification token
expiresAttimestampNOT NULLToken expiry
createdAttimestampCreation timestamp
updatedAttimestampLast update

WhatsApp account connections (app-managed).

ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Account UUID
userIdvarchar(36)FK → user.id, NOT NULLOwner user
nametextNOT NULLAccount display name
phoneNumbertextNOT NULLWhatsApp phone number
avatartextProfile avatar URL
colortextNOT NULLUI color identifier
statustextDEFAULT ‘active’Account status
isBusinessAccountbooleanNOT NULLWhatsApp Business flag
isVerifiedbooleanNOT NULLVerified badge status
accountNumberintegerNOT NULLSequential number per user
createdAttimestampNOT NULLCreation timestamp

Indexes:

  • idx_accounts_userId on userId
  • Composite index on (userId, accountNumber)

User subscription records.

ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Subscription UUID
userIdvarchar(36)FK → user.id, UNIQUE, NOT NULLOwner user
planTypetextNOT NULLPlan: ‘free’, ‘pro_monthly’, ‘pro_yearly’
statustextNOT NULLStatus: ‘active’, ‘canceled’, ‘expired’
creemCustomerIdtextUNIQUECreem customer ID
creemSubscriptionIdtextUNIQUECreem subscription ID
creemCheckoutIdtextUNIQUECheckout ID (idempotency)
currentPeriodStarttimestampBilling period start
currentPeriodEndtimestampBilling period end
canceledAttimestampCancellation timestamp
createdAttimestampNOT NULLCreation timestamp
updatedAttimestampNOT NULLLast update

Constraint: One subscription per user (UNIQUE on userId)


Chat priority configuration per account.

ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Priority UUID
nametextNOT NULLPriority name (e.g., “High”)
colortextNOT NULLUI color
accountIduuidFK → accounts.id, NOT NULLParent account
createdAttimestampNOT NULLCreation timestamp

Chat organization folders per account.

ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Folder UUID
nametextNOT NULLFolder name
colortextNOT NULLUI color
accountIduuidFK → accounts.id, NOT NULLParent account
createdAttimestampNOT NULLCreation timestamp

Message templates per account.

ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Template UUID
nametextNOT NULLTemplate name
contenttextNOT NULLTemplate message content
accountIduuidFK → accounts.id, NOT NULLParent account
createdAttimestampNOT NULLCreation timestamp

Tags assigned to chats.

ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Tag UUID
chatIdtextNOT NULLWhatsApp chat ID
tagtextNOT NULLTag name
accountIduuidFK → accounts.id, NOT NULLParent account
createdAttimestampNOT NULLCreation timestamp

Unique: (chatId, accountId) - one tag per chat per account


Priority assignments to chats.

ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Assignment UUID
chatIdtextNOT NULLWhatsApp chat ID
priorityIduuidFK → priorities.id, NOT NULLPriority reference
accountIduuidFK → accounts.id, NOT NULLParent account
createdAttimestampNOT NULLCreation timestamp

Unique: (chatId, accountId) - one priority per chat per account


Folder assignments to chats (many-to-many).

ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Assignment UUID
chatIdtextNOT NULLWhatsApp chat ID
folderIduuidFK → folders.id, NOT NULLFolder reference
accountIduuidFK → accounts.id, NOT NULLParent account
createdAttimestampNOT NULLCreation timestamp

WhatsApp Web.js session metadata.

ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Session UUID
accountIduuidFK → accounts.id, UNIQUE, NOT NULLParent account
sessionDatajsonbEncrypted session data
statustextDEFAULT ‘disconnected’Connection status
lastConnectedtimestampLast successful connection
createdAttimestampNOT NULLCreation timestamp
updatedAttimestampNOT NULLLast update

Subscribed newsletters/channels.

ColumnTypeConstraintsDescription
iduuidPRIMARY KEY, DEFAULT gen_random_uuid()Newsletter UUID
accountIduuidFK → accounts.id, NOT NULLParent account
channelIdtextNOT NULLWhatsApp channel ID
nametextChannel name
subscribedAttimestampNOT NULLSubscription date
createdAttimestampNOT NULLCreation timestamp
┌────────────┐
│ user │
├────────────┤
│ id (PK) │◄─────────────────┬─────────────────┬─────────────────┐
│ name │ │ │ │
│ email │ │ │ │
│ ... │ │ │ │
└────────────┘ │ │ │
│ │ │ │
│ 1:N │ 1:N │ 1:1 │ 1:N
▼ │ │ │
┌────────────┐ ┌─────┴──────┐ ┌─────┴──────┐ ┌─────┴──────┐
│ session │ │ account │ │subscription│ │ accounts │
├────────────┤ │(OAuth/Auth)│ ├────────────┤ │ (WhatsApp) │
│ id (PK) │ └────────────┘ │ id (PK) │ ├────────────┤
│ userId(FK) │ │ userId(FK) │ │ id (PK) │
│ token │ │ planType │ │ userId(FK) │
│ ... │ │ status │ │ phoneNumber│
└────────────┘ │ ... │ │ ... │
└────────────┘ └────────────┘
┌───────────────────┬───────────────────┼───────────────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ priorities │ │ folders │ │ templates │ │ chat_tags │
├────────────┤ ├────────────┤ ├────────────┤ ├────────────┤
│ id (PK) │ │ id (PK) │ │ id (PK) │ │ id (PK) │
│ accountId │ │ accountId │ │ accountId │ │ accountId │
│ name │ │ name │ │ content │ │ chatId │
│ color │ │ color │ │ ... │ │ tag │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
│ │
▼ ▼
┌────────────┐ ┌────────────┐
│chat_priorit│ │chat_folders│
├────────────┤ ├────────────┤
│ id (PK) │ │ id (PK) │
│ priorityId │ │ folderId │
│ chatId │ │ chatId │
│ accountId │ │ accountId │
└────────────┘ └────────────┘
MigrationDescription
0000Initial schema (user, session, account, verification)
0001Add WhatsApp accounts table
0002Add subscriptions table
0003Add priorities table
0004Add folders table
0005Add templates table
0006Add chat_tags table
0007Add chat_priorities table
0008Add chat_folders table
0009Add whatsapp_sessions table
0010Add newsletters table
0011Add indexes for performance
0012Add Creem payment fields to subscriptions
0013Add checkout_id for idempotency
0014Add cascade delete rules
drizzle.config.ts
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!
}
})
const userWithSub = await db
.select()
.from(users)
.leftJoin(subscriptions, eq(users.id, subscriptions.userId))
.where(eq(users.id, userId))
const folders = await db
.select()
.from(foldersTable)
.leftJoin(chatFolders, eq(foldersTable.id, chatFolders.folderId))
.where(inArray(foldersTable.accountId, accountIds))
const counts = await db
.select({
accountId: templates.accountId,
count: count()
})
.from(templates)
.groupBy(templates.accountId)
.where(inArray(templates.accountId, accountIds))