Email deliverability
Deliverability is managed through a two-level sender hierarchy and four independent send caps.
Sender hierarchy
flowchart TD Sub["Subscription"] --> D1["Domain\npayability.com\ndailyCap 2000"] Sub --> D2["Domain\nsales.payability.com\ndailyCap 1000"] D1 --> A1["EmailSendingDomain\nalex@payability.com\ndailyCap 300"] D1 --> A2["EmailSendingDomain\nsam@payability.com\ndailyCap 300"] D2 --> A3["EmailSendingDomain\njo@sales.payability.com\ndailyCap 250"]
Domain-
One row per Resend-verified domain, unique on
(subscriptionId, name). HoldsresendDomainId,resendStatus, a shareddailyCap, awarmupStatuslabel (warming_uporactive),isActive, and atotalSentCountthat is never reset. The domain’s usage today is the sum of its accounts' sends today — it has no counter of its own. EmailSendingDomain("email account" in the UI)-
A concrete
Fromaddress under a parent domain. HoldsfromName,fromEmail,domain,resendDomainId,isActive,isVerified, its owndailyCap,dailySentCountanddailySentDate, a lifetimetotalSentCount, and per-event counters.domainIdis nullable because it was added after the table existed and legacy rows are backfilled by hand.
|
Subdomains are not modelled as children of their parent domain. In Resend each subdomain is its own
verified domain with its own domain id, so |
The four caps
Checked in this order; the first breach stops the send.
| Level | Field | Enforcement |
|---|---|---|
Campaign per day |
|
|
Variation per hour |
|
|
Account per day |
|
|
Domain per day |
|
Same query, comparing the sum of the domain’s active accounts' sends today against the parent cap |
null means unlimited at every level.
Rolling windows are used for the per-variation rate rather than clock hours because a clock-hour reset produces a burst at the top of every hour, which is exactly the pattern spam filters look for.
Date-scoped daily counters
dailySentCount only counts for the UTC day stored in dailySentDate. incrementSendingDomainDailyCount
does one atomic UPDATE: when dailySentDate is today it adds 1, otherwise it restarts the counter at 1
and stamps today’s date. Every reader goes through effectiveDailySent() in
lib/utils/email-daily-cap.ts, which treats a counter from an earlier day as 0:
-
findEligibleSendingDomains, for both the account cap and the domain total -
getDomainSummary(sentToday) and the email-domain serializers (daily_sent_count,daily_sent_total)
So a missed reset can no longer leave accounts stuck over their cap.
Daily reset
The Inngest function sendingDomainDailyResetCron runs at 00:00 UTC, zeroes dailySentCount and clears
dailySentDate on every account. It is a backstop; the date-scoped counters already roll over on their
own. totalSentCount on both the account and its parent domain is never reset.
Daily capacity
The Sending page’s daily capacity (getDomainSummary.dailyCapacity) adds, for each active domain, the
lower of Domain.dailyCap and the sum of its active accounts' caps. A domain with a 3,000 cap but one
1,000-cap account contributes 1,000. A domain where both sides are unlimited contributes 0.
Counters
Incremented by the Resend webhook handlers (see Resend email events):
| Counter | Incremented on | Notes |
|---|---|---|
|
Resend hand-off in |
Counts only for |
|
Resend hand-off in |
Lifetime, never reset |
|
|
Lifetime provider-confirmed sends |
|
|
Inbound replies to this account |
|
first |
Deduplicated by |
|
first |
Deduplicated by |
|
|
|
|
|
email.delivered deliberately does not increment sentCount — email.sent already did, and
counting both would double every send.
Opens and clicks are counted only on their first occurrence per recipient, so the numbers are unique-recipient counts rather than raw event counts.
Suppression
Suppression lives on TargetEmails, the per-workspace address registry — not on a separate
suppression table, and not per campaign. An address is suppressed when any of these is true:
| Flag | Set by |
|---|---|
|
|
|
The |
|
Permanent |
oneToOneOptOut exists for direct one-to-one email and is not part of the campaign suppression
check.
Suppression is enforced twice:
-
At lease time —
fetchAndLockPendingTargetsjoinstarget_emailsand requiresnewsletterOptOut = false AND isBlocked = false, so suppressed addresses never enter the queue. -
At send time —
emailSendstepcheck-eligibilitycallsgetTargetEmailEligibility.isBlockedsets the campaign target toblockedand copiesblockReasonintolastError. Newsletter or marketing opt-out, when the address is not blocked, setsunsubscribed.
The double check exists because minutes pass between the two, and a recipient may unsubscribe from a different campaign’s email in that window.
A spam complaint sets marketingOptOut and isBlocked, which is stricter than an unsubscribe:
a complaint is a deliverability risk to the whole domain, not just a preference. A Permanent bounce
sets only isBlocked plus blockReason, because the address cannot receive mail. The bounced
campaign row stays status bounced so deliverability charts do not count it as a complaint.
Resend topics
Campaign.resendTopicId links a campaign to a Resend topic, giving audience segmentation and
platform-managed unsubscribe alongside the application’s own. lib/services/resend-topic-service.ts
manages topics, and emailSend passes topicId on every send.
Reply-to
buildReplyToForSendingDomain({ fromEmail, fromName }) in lib/utils/emails.ts derives the
Reply-To header from the sending account, so replies land on an address the inbox can correlate
back to a campaign. See Email inbox and replies.
Operating checklist
-
Verify the domain in Resend, then create the
Domainrow with itsresendDomainId(GET /api/email-domains/resend-domainslists verified domains). -
Add at least one active, verified
EmailSendingDomainunder it. -
Set
dailyCapon both levels. During warm-up, raise the account caps gradually and leavewarmupStatusatwarming_upas a signal to other operators. -
Watch
bouncedCountandunsubscribedCountper account — a single bad account can damage the reputation of the whole domain. -
GET /api/email-domains/statsandGET /api/email-domains/summaryback the dashboard views.