Relevant source files
The following files were used as context for generating this wiki page:
- packages/email/src/send-via-resend.ts
- packages/email/src/templates/campaign-email.tsx
- apps/web/lib/email/email-templates-map.ts
- apps/web/app/ee/api/campaigns/campaignId/preview/route.ts
- packages/email/src/templates/lead-status-updated.tsx
- packages/email/src/templates/new-submitted-lead-comments-from-program.tsx
- apps/web/app/ee/api/cron/send-batch-email/route.ts
- packages/email/src/templates/domain-deleted.tsx
- packages/email/src/templates/broadcasts/dub-product-update-summer26.tsx
- apps/web/lib/partners/create-stripe-transfer.ts
- packages/email/src/resend/constants.ts
- packages/email/src/templates/broadcasts/dub-startup-program-announcement.tsx
- packages/email/src/templates/webhook-failed.tsx
- packages/email/src/templates/webhook-disabled.tsx
- packages/email/src/templates/verify-email.tsx
- packages/email/src/templates/domain-renewal-failed.tsx
- packages/email/src/templates/email-domain-status-changed.tsx
- packages/email/src/templates/api-key-created.tsx
- packages/email/src/templates/new-message-from-program.tsx
- apps/web/app/app.dub.co/dashboard/slug/ee/settings/domains/email/page-client.tsx
- packages/email/src/templates/broadcasts/dub-wrapped.tsx
- packages/email/src/templates/bounty-new-submission.tsx
- packages/email/src/templates/dub-partner-rewind.tsx
- packages/email/src/templates/new-message-from-partner.tsx
- packages/email/src/index.ts
- packages/email/src/templates/domain-expired.tsx
- packages/email/src/templates/new-submitted-lead-comments-from-partner.tsx
- packages/email/src/templates/domain-transferred.tsx
- packages/email/src/templates/domain-renewal-reminder.tsx
- packages/email/src/templates/domain-renewed.tsx
Overview
Email notification infrastructure in Dub handles programmatic dispatch across security, lifecycle, marketing, and partner communication channels using React Email templates styled with Tailwind CSS. The system provides robust dispatch capabilities via Resend with automatic fallback to Nodemailer when Resend is unconfigured, while rewriting reserved test domains and managing sender variants such as primary, notifications, and marketing.
Sources: packages/email/src/send-via-resend.ts:8-33, packages/email/src/resend/constants.ts:1-4, packages/email/src/index.ts:6-29
Email dispatch architecture
Transport selection and public entrypoints
Public email dispatch is exposed through sendEmail and sendBatchEmail in packages/email/src/index.ts. These functions evaluate runtime configuration to select between the Resend SDK and an SMTP transport via Nodemailer. If the resend client is initialized, dispatch is delegated to sendEmailViaResend or sendBatchEmailViaResend. When Resend is unconfigured, the system checks whether SMTP_HOST and SMTP_PORT are present in the environment variables. If SMTP is configured, single messages dispatch through sendViaNodeMailer, whereas batch operations process concurrently via Promise.all mapping over the input array and generating random UUIDs for response simulation. If neither transport is configured, dispatch aborts and logs an informational message.
Sources: packages/email/src/index.ts:6-71
Sender variants and recipient filtering
Email options map logical variants to designated sender addresses. The system recognizes three variants (primary, notifications, and marketing), mapping each to its default sender string. When processing outgoing payloads, the sender defaults to the mapped variant address if an explicit from address is omitted.
| Variant | Default Sender Address |
|---|---|
primary | Dub.co <system@dub.co> |
notifications | Dub.co <notifications@mail.dub.co> |
marketing | Steven from Dub.co <steven@ship.dub.co> |
Sources: packages/email/src/resend/constants.ts:1-4, packages/email/src/send-via-resend.ts:40-41, packages/email/src/send-via-resend.ts:60
Recipient addresses undergo validation against reserved test domains to prevent 422 validation errors from Resend. The blocked domains include example.com, example.net, example.org, and test.com. If a recipient domain matches any blocked entry or its subdomain, rewriteBlockedRecipient rewrites the address to delivered+username@resend.dev. Furthermore, when VERCEL_ENV is set to preview, recipient addresses are forcibly overridden to delivered@resend.dev, and git branch references are appended to email subjects in the format [gitBranch].
Warning
In Vercel preview environments, recipient rewriting forces all outbound messages to delivered@resend.dev regardless of the input parameter, ensuring test runs do not leak external mail.
Sources: packages/email/src/send-via-resend.ts:8-33, packages/email/src/send-via-resend.ts:53-61
Transactional notification templates
Authentication and workspace lifecycle components
Transactional email templates are built using @react-email/components and styled with Tailwind CSS, wrapped inside an Html, Head, Preview, and Tailwind structure alongside a shared Footer component. The VerifyEmail template manages user authentication by rendering a 6-digit verification code (code) inside a monospace, letter-spaced container (tracking-[0.25em] font-mono text-2xl). The template explicitly states that the code expires in 10 minutes.
Sources: packages/email/src/templates/verify-email.tsx:16-52
Workspace administrative actions generate security alerts. The APIKeyCreated template renders confirmation notices when a new API key is provisioned within a workspace. It displays the workspace name, token name, token type, and permissions, accompanied by a timestamp formatted via formatDate(new Date().toString()). It provides links directing users to manage or delete unauthorized keys at https://app.dub.co/${workspace.slug}/settings/tokens.
Sources: packages/email/src/templates/api-key-created.tsx:17-87
export default function VerifyEmail({
email = "panic@thedis.co",
code = "123456",
}: {
email: string;
code: string;
}) {Sources: packages/email/src/templates/verify-email.tsx:16-22
Webhook failure and deactivation templates
Webhook health monitoring utilizes two distinct email templates (WebhookFailed and WebhookDisabled) to notify workspace administrators of delivery disruptions. Both templates accept a workspace object containing name and slug, and a webhook object containing id, url, and a disableThreshold integer. WebhookFailed additionally accepts consecutiveFailures.
Sources: packages/email/src/templates/webhook-failed.tsx:17-41, packages/email/src/templates/webhook-disabled.tsx:17-39
| Template File | Component Name | Key Props | Action URL |
|---|---|---|---|
verify-email.tsx | VerifyEmail | email, code | N/A (Verification page) |
api-key-created.tsx | APIKeyCreated | email, workspace, token | https://app.dub.co/{slug}/settings/tokens |
webhook-failed.tsx | WebhookFailed | email, workspace, webhook | https://app.dub.co/{slug}/settings/webhooks/{id}/edit |
webhook-disabled.tsx | WebhookDisabled | email, workspace, webhook | https://app.dub.co/{slug}/settings/webhooks/{id}/edit |
Sources: packages/email/src/templates/verify-email.tsx:16-22, packages/email/src/templates/api-key-created.tsx:17-39, packages/email/src/templates/webhook-failed.tsx:17-41, packages/email/src/templates/webhook-disabled.tsx:17-39
Note
The WebhookFailed template warns that the target endpoint has failed consecutiveFailures times and will automatically deactivate once it reaches the disableThreshold, whereas WebhookDisabled confirms that deactivation has already occurred after hitting that exact threshold sequentially.
Sources: packages/email/src/templates/webhook-failed.tsx:55-59, packages/email/src/templates/webhook-disabled.tsx:53-57
Domain lifecycle notifications
Domain renewal and expiration workflows
Domain lifecycle notification templates handle custom link domains, domain renewals, expirations, deletions, transfers, and email sending domain verifications. The EmailDomainStatusChanged template monitors verification states using an EmailDomainStatus union type consisting of pending, verified, failed, temporary_failure, not_started, partially_verified, and partially_failed.
Sources: packages/email/src/templates/email-domain-status-changed.tsx:17-24
| Status Category | Matching EmailDomainStatus Values | Rendered Heading & Action |
|---|---|---|
| Verified | verified | "Your email domain has been successfully verified" |
| Failed | failed, temporary_failure, partially_failed | "Your email domain verification has failed" |
| Pending | pending, not_started, partially_verified | "Your email domain status has changed" (includes pending notice) |
Sources: packages/email/src/templates/email-domain-status-changed.tsx:39-53
Note
The EmailDomainStatusChanged template checks whether newStatus evaluates to verified, failed (covering permanent, temporary, or partial failures), or pending (covering pending, not started, or partial verification states) to customize alert previews and guidance text.
Sources: packages/email/src/templates/email-domain-status-changed.tsx:39-47
Automated domain renewals and expirations use dedicated transactional email templates to communicate billing outcomes, upcoming charges, and deletion policies. The DomainRenewalReminder component notifies users of an upcoming renewal fee (renewalFee), expiry date (expiresAt), reminder window (reminderWindow), and scheduled charge timing (chargeAt and chargeAtInText).
Sources: packages/email/src/templates/domain-renewal-reminder.tsx:17-43
When payment processing fails, DomainRenewalFailed generates an itemized table showing each failing domain slug and its expiration date, notifying recipients that automated attempts will retry in 3 days. Similarly, DomainExpired lists expired domains and directs administrators to the domain settings page. If a custom domain remains invalid for 30 days, DomainDeleted informs the workspace owner that the domain has been permanently removed from Dub. Finally, DomainTransferred notifies stakeholders when a domain and its associated links are moved to a new workspace.
Sources: packages/email/src/templates/domain-renewal-failed.tsx:20-67, packages/email/src/templates/domain-expired.tsx:19-70, packages/email/src/templates/domain-deleted.tsx:17-50, packages/email/src/templates/domain-transferred.tsx:18-54
Partner and marketplace communications
Partner payouts and program messaging triggers
Asynchronous routines in the partner system manage financial disbursements and communication threads between programs, partners, and workspace administrators. When a partner transfer is processed via Stripe, createStripeTransfer.ts uses waitUntil and Promise.allSettled to execute commission status tracking and queue referral jobs via enqueueBatchJobs pointing to ${APP_DOMAIN_WITH_NGROK}/api/cron/commissions/referrals/queue.
Sources: apps/web/lib/partners/create-stripe-transfer.ts:303-321
Note
Payout email subjects dynamically branch on forceWithdrawal. If forced, the subject states "A withdrawal of [amount] has been initiated from your Dub account" using PartnerPayoutForceWithdrawal with method "connect"; otherwise, it outputs "You've received a [amount] payout from [program name]" via PartnerPayoutProcessed.
Sources: apps/web/lib/partners/create-stripe-transfer.ts:328-343
Program and partner messaging templates render interactive communication streams using React Email components. NewMessageFromProgram and NewMessageFromPartner enforce a constant MAX_DISPLAYED_MESSAGES limit of 3 for rendered message rows, displaying fallback avatars from OG_AVATAR_URL when user images are null.
Sources: packages/email/src/templates/new-message-from-program.tsx:24-24, packages/email/src/templates/new-message-from-partner.tsx:24-24
| Template Name | Primary Recipient | Key Props & Identifiers | Call-to-Action Target |
|---|---|---|---|
NewMessageFromProgram | Partner | program, messages[], email | https://partners.dub.co/messages/${program.slug} |
NewMessageFromPartner | Workspace Admin | workspaceSlug, partner, messages[], email | https://app.dub.co/${workspaceSlug}/program/messages/${partner.id} |
NewBountySubmission | Workspace Admin | workspace, bounty, partner, submission, email | https://app.dub.co/${workspace.slug}/program/bounties/${bounty.id}?submissionId=${submission.id} |
LeadStatusUpdated | Partner | partner, program, lead, notes | Profile notification settings URL |
Sources: packages/email/src/templates/new-message-from-program.tsx:76-92, packages/email/src/templates/new-message-from-partner.tsx:56-69, packages/email/src/templates/bounty-new-submission.tsx:36-54, packages/email/src/templates/lead-status-updated.tsx:33-44
Submitted lead comments and status updates
Lead submission workflows support bi-directional commentary between programs and partners via dedicated card wrappers and routing templates. NewSubmittedLeadCommentsFromProgram links recipients to https://partners.dub.co/programs/${program.slug}/leads?leadId=${lead.id}, while NewSubmittedLeadCommentsFromPartner routes administrators to https://app.dub.co/${workspace.slug}/program/leads?leadId=${lead.id}.
Sources: packages/email/src/templates/new-submitted-lead-comments-from-program.tsx:62-62, packages/email/src/templates/new-submitted-lead-comments-from-partner.tsx:59-59
Warning
When rendering lead status updates through LeadStatusUpdated, if optional notes are supplied, the template injects an additional text block displaying custom remarks from the program alongside the updated lead status (lead.status) and company metadata (lead.company).
Sources: packages/email/src/templates/lead-status-updated.tsx:67-76
Campaign authoring and broadcasts
Dynamic broadcast templates and campaign preview routes handle audience email generation. The CampaignEmail component renders structured campaign bodies with customizable programs and types (transactional or marketing), using CAMPAIGN_EMAIL_TEXT_COLOR set to #000000 for inline text styling. When the campaign type is marketing, an opt-out footer is appended with a link pointing to https://partners.dub.co/profile/notifications. For transactional campaigns where program.messagingEnabledAt is active, a "Reply in Dub" button is rendered linking to https://partners.dub.co/messages/${program.slug}.
Sources: packages/email/src/templates/campaign-email.tsx:18-20, packages/email/src/templates/campaign-email.tsx:92-118
Preview generation is handled by the POST /api/campaigns/[campaignId]/preview route, protected by workspace authentication (withWorkspace) with required plans ["advanced", "enterprise"] and roles ["owner", "member"]. Request payloads are validated against sendPreviewEmailSchema, which enforces a maximum of 10 email addresses via z.email().
Sources: apps/web/app/ee/api/campaigns/campaignId/preview/route.ts:30-34, apps/web/app/ee/api/campaigns/campaignId/preview/route.ts:37-42, apps/web/app/ee/api/campaigns/campaignId/preview/route.ts:129-132
Caution
When a custom from address is supplied in the preview payload, the route validates it using parseCampaignFromAddress and ensures the sending domain matches one of the program's verified emailDomains. If the domain is unverified, a DubApiError with code bad_request is thrown.
Sources: apps/web/app/ee/api/campaigns/campaignId/preview/route.ts:65-81
Broadcast templates like DubWrapped, DubProductUpdateSummer26, DubStartupProgramAnnouncement, and DubPartnerRewind offer specialized annual reviews and announcements. DubWrapped displays granular statistics (stats), top links with custom click intervals (topLinks), and country distributions (topCountries) utilizing nFormatter and flag images from wsrv.nl with hatscripts.github.io/circle-flags.
Sources: packages/email/src/templates/broadcasts/dub-wrapped.tsx:28-75, packages/email/src/templates/broadcasts/dub-wrapped.tsx:342-346
Batch processing and domain management
Batch dispatch routes and template mapping
Asynchronous batch email delivery is handled by the POST /api/cron/send-batch-email route, which requires QStash signature verification via verifyQstashSignature on the raw request body. Incoming payloads are validated using a Zod schema (batchEmailPayloadSchema) that accepts an array of email objects. Each object specifies a template name, template properties, recipient address (to), optional sender (from), subject, bcc, reply-to, variant (primary, notifications, or marketing), headers, tags, and scheduled timestamp.
Sources: apps/web/app/ee/api/cron/send-batch-email/route.ts:13-46
The system maps template string identifiers to underlying React-email components using EMAIL_TEMPLATES_MAP. The registered templates span partner rewards, payouts, risk summaries, system notifications, and broadcasts.
| Template Key | Source Component / Import |
|---|---|
BountyApproved | BountyApproved |
ConnectPayoutReminder | ConnectPayoutReminder |
ConnectPlatformsReminder | ConnectPlatformsReminder |
PartnerPayoutConfirmed | PartnerPayoutConfirmed |
PartnerPayoutProcessed | PartnerPayoutProcessed |
PartnerDeactivated | PartnerDeactivated |
PartnerBanned | PartnerBanned |
ProgramPayoutThankYou | ProgramPayoutThankYou |
UnresolvedRiskEventsSummary | UnresolvedRiskEventsSummary |
PartnerGroupChanged | PartnerGroupChanged |
PartnerRewardUpdated | PartnerRewardUpdated |
WorkspaceDisabled | WorkspaceDisabled |
DubStartupProgramAnnouncement | DubStartupProgramAnnouncement |
DubProductUpdateSummer26 | DubProductUpdateSummer26 |
Sources: apps/web/lib/email/email-templates-map.ts:1-32
Warning
If a template name provided in the batch payload is missing from EMAIL_TEMPLATES_MAP, Promise.allSettled catches the error per item. If every item in the batch fails processing or if the subsequent sendBatchEmail call returns a Resend API error, the route logs the failure with mention flags and returns a 500 status response.
Sources: apps/web/app/ee/api/cron/send-batch-email/route.ts:54-173
Dashboard email domain administration
Workspace email domain administration is managed client-side through the EmailDomains component located in the workspace settings. The interface checks workspace capabilities using getPlanCapabilities to enforce whether email campaign sending is allowed for the active plan.
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/settings/domains/email/page-client.tsx:23-35
When a workspace lacks permission to send email campaigns, an animated empty state directs users to an upgrade path (/[slug]/upgrade) with a link to help documentation. When domains are permitted, the client monitors searchParams for DomainConnect callback states (domain_connect=callback). If errors occur (such as user_cancel), toast notifications alert the user; otherwise, successful submissions trigger staged SWR cache revalidations for verification endpoints (/api/email-domains/*/verify and /api/email-domains) at 0, 4000, and 10000 milliseconds before cleaning up query parameters.
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/settings/domains/email/page-client.tsx:36-105
Related
On This Page
OverviewEmail dispatch architectureTransport selection and public entrypointsSender variants and recipient filteringTransactional notification templatesAuthentication and workspace lifecycle componentsWebhook failure and deactivation templatesDomain lifecycle notificationsDomain renewal and expiration workflowsPartner and marketplace communicationsPartner payouts and program messaging triggersSubmitted lead comments and status updatesCampaign authoring and broadcastsBatch processing and domain managementBatch dispatch routes and template mappingDashboard email domain administrationRelated