Email models

EmailCampaignTarget

Table email_campaign_targets. One contact enrolled in one email campaign.

Field Type Notes

campaignId

String @db.Uuid

contactId

String @db.Uuid

Required — imports upsert the contact first

subscriptionId

String @db.Uuid

emailId

String @db.Uuid

Required FK to target_emails; carries suppression and cadence

email, firstName, lastName

String / String?

Cached from the contact at import time, so the recipient list renders without a join

status

EmailTargetStatus

default pending

attempts

Int

default 0

lastError

String?

sentAt

DateTime?

Set at Resend hand-off, refreshed by email.delivered

openedAt, clickedAt, unsubscribedAt

DateTime?

First occurrence only for open and click

resendEmailId

String?

Correlation key for outbound webhooks

variationId

String? @db.Uuid

Which variation was sent

sendingDomainId

String? @db.Uuid

Which account sent it

queuedAt

DateTime?

Lease stamp; lock window is 2 days (EMAIL_TARGET_LOCK_WINDOW_SECONDS)

hubspotSynced

Boolean

default false; true once outbound CRM activity was queued

category

EmailTargetCategory?

LLM reply classification

categorisedAt

DateTime?

categoryModel

String?

Audit: which model or label produced category

Unique: (campaignId, contactId) and (campaignId, email) — one enrollment per person per campaign, while the same address may be a recipient of many campaigns.

Indexes: (campaignId, status), (campaignId, status, queuedAt) for the leasing query, (campaignId, sentAt), emailId, subscriptionId, contactId, email, resendEmailId.

Relations: campaign, contact, subscription, variation, targetEmail, emails, contactSequences.

A click sets clickedAt but does not set a status — there is no clicked value in EmailTargetStatus. Filter recipients on has_clicked, not on status.

EmailVariation

Table email_variations. Subject plus HTML body for a campaign, or a reusable template.

Field Type Notes

campaignId

String? @db.Uuid

NULL ⇒ a campaign-less reusable template

subscriptionId

String @db.Uuid

name, subject

String

htmlBody

String (Text)

resendTemplateId

String?

Created programmatically; synced on create and update. Null on templates until they are copied into a campaign

templateType

String

Always "custom"

templateVariables

Json

[{ key, type, fallbackValue }]; keys map to contact fields at send time

status

EmailVariationStatus

pending_generation, active, archived

usageCount

Int

Lifetime sends; a statistic, never a gate

queuedAt

DateTime?

Reserved for future scheduling

Every send and selection query filters by campaignId, so a template can never be sent directly.

UNSUBSCRIBE_LINK is injected automatically at send time and is applied last, so no user-defined variable can overwrite it.

Domain

Table domains. A Resend-verified sending domain.

Field Type Notes

subscriptionId

String @db.Uuid

Unique with name

name

String

e.g. payability.com or sales.payability.com

resendDomainId

String?

Resend domain id

resendStatus

String?

Mirror of the Resend status at link time

dailyCap

Int?

Shared cap across all its accounts; null = unlimited

warmupStatus

DomainWarmupStatus

warming_up or active; a label only

isActive

Boolean

Only accounts under an active domain may send

totalSentCount

Int

Lifetime, never reset by the cron

Unique: (subscriptionId, name).

The domain has no daily counter of its own — its usage today is the sum of its accounts' dailySentCount for those whose dailySentDate is today (UTC).

Each Resend subdomain is its own verified domain with its own id, so sales.payability.com is a separate Domain row rather than a child of payability.com.

EmailSendingDomain

Table email_sending_domains. A concrete From address ("email account" in the UI).

Field Type Notes

subscriptionId

String @db.Uuid

domainId

String? @db.Uuid

Parent Domain; nullable until legacy rows are backfilled

domain

String

Host, must match the parent Domain.name

fromName, fromEmail

String

e.g. Alex, alex@payability.com

resendDomainId

String?

isActive, isVerified

Boolean

Only active accounts are selected

dailyCap

Int?

Per-account cap; null = unlimited

dailySentCount

Int

Incremented at Resend hand-off; counts only for dailySentDate; also reset nightly

dailySentDate

DateTime? @db.Date

UTC day dailySentCount belongs to. An earlier day (or null) means 0 sent today

totalSentCount

Int

Incremented with dailySentCount; never reset

sentCount

Int

Lifetime, incremented on email.sent

receivedCount

Int

On email.received

openedCount

Int

On the first email.opened per target

clickedCount

Int

On the first email.clicked per target

bouncedCount

Int

On email.bounced

unsubscribedCount

Int

On email.unsubscribed

dailySentCount and sentCount count different moments on purpose: the first is bumped when the message is handed to Resend (so the cap reflects committed sends immediately), the second when Resend confirms it sent. email.delivered increments neither, because email.sent already did.

Emails

Table emails. Individual messages, inbound and outbound.

Field Type Notes

subscriptionId

String @db.Uuid

sendingDomainId

String? @db.Uuid

Which account sent or received

contactId, campaignId, emailCampaignTargetId

String? @db.Uuid

Correlation into the contact graph

threadId

String @db.Uuid

Shared by every message in a conversation

direction

EmailDirection

inboxCategory

EmailInboxCategory?

In-app folder

category

EmailTargetCategory?

LLM classification mirrored from the campaign target

externalId

String

Resend email_id, inbound or outbound

inReplyToExternalId

String?

Parent id for reply correlation

toEmail, fromEmail, subject

String

body

String (Text)

Plain text

bodyHtml

String? (Text)

opened

Boolean

The recipient’s tracking pixel fired

readAt

DateTime?

An operator read it in the app

archivedAt

DateTime?

sentAt

DateTime?

Unique: (subscriptionId, externalId).

Indexes: (subscriptionId, createdAt DESC) for the inbox list, (subscriptionId, threadId, createdAt) for thread rendering, (subscriptionId, archivedAt), toEmail, fromEmail, contactId, emailCampaignTargetId.

Relations: subscription, sendingDomain, contact, campaign, emailCampaignTarget, contactSequenceSteps.

opened and readAt are separate because they describe different actors — the recipient and the operator.