---
title: "Email Notification"
description: "Email notification infrastructure in Dub handles programmatic dispatch across security, lifecycle, marketing, and partner communication channels using React Email templates styled with Tailwind CSS..."
last_updated: "2026-10-12T07:00:10.139631+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/background-processing/email-notification"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/email/src/send-via-resend.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/send-via-resend.ts)
- [packages/email/src/templates/campaign-email.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/campaign-email.tsx)
- [apps/web/lib/email/email-templates-map.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/email/email-templates-map.ts)
- [apps/web/app/ee/api/campaigns/campaignId/preview/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/campaigns/%5BcampaignId%5D/preview/route.ts)
- [packages/email/src/templates/lead-status-updated.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/lead-status-updated.tsx)
- [packages/email/src/templates/new-submitted-lead-comments-from-program.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/new-submitted-lead-comments-from-program.tsx)
- [apps/web/app/ee/api/cron/send-batch-email/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/send-batch-email/route.ts)
- [packages/email/src/templates/domain-deleted.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/domain-deleted.tsx)
- [packages/email/src/templates/broadcasts/dub-product-update-summer26.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/broadcasts/dub-product-update-summer26.tsx)
- [apps/web/lib/partners/create-stripe-transfer.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/partners/create-stripe-transfer.ts)
- [packages/email/src/resend/constants.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/resend/constants.ts)
- [packages/email/src/templates/broadcasts/dub-startup-program-announcement.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/broadcasts/dub-startup-program-announcement.tsx)
- [packages/email/src/templates/webhook-failed.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/webhook-failed.tsx)
- [packages/email/src/templates/webhook-disabled.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/webhook-disabled.tsx)
- [packages/email/src/templates/verify-email.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/verify-email.tsx)
- [packages/email/src/templates/domain-renewal-failed.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/domain-renewal-failed.tsx)
- [packages/email/src/templates/email-domain-status-changed.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/email-domain-status-changed.tsx)
- [packages/email/src/templates/api-key-created.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/api-key-created.tsx)
- [packages/email/src/templates/new-message-from-program.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/new-message-from-program.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/domains/email/page-client.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/domains/email/page-client.tsx)
- [packages/email/src/templates/broadcasts/dub-wrapped.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/broadcasts/dub-wrapped.tsx)
- [packages/email/src/templates/bounty-new-submission.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/bounty-new-submission.tsx)
- [packages/email/src/templates/dub-partner-rewind.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/dub-partner-rewind.tsx)
- [packages/email/src/templates/new-message-from-partner.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/new-message-from-partner.tsx)
- [packages/email/src/index.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/index.ts)
- [packages/email/src/templates/domain-expired.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/domain-expired.tsx)
- [packages/email/src/templates/new-submitted-lead-comments-from-partner.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/new-submitted-lead-comments-from-partner.tsx)
- [packages/email/src/templates/domain-transferred.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/domain-transferred.tsx)
- [packages/email/src/templates/domain-renewal-reminder.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/domain-renewal-reminder.tsx)
- [packages/email/src/templates/domain-renewed.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/domain-renewed.tsx)
</details>

## 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/send-via-resend.ts#L8-L33), [packages/email/src/resend/constants.ts:1-4](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/resend/constants.ts#L1-L4), [packages/email/src/index.ts:6-29](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/index.ts#L6-L29)

## 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/index.ts#L6-L71)

### 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/resend/constants.ts#L1-L4), [packages/email/src/send-via-resend.ts:40-41](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/send-via-resend.ts#L40-L41), [packages/email/src/send-via-resend.ts:60](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/send-via-resend.ts#L60)

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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/send-via-resend.ts#L8-L33), [packages/email/src/send-via-resend.ts:53-61](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/send-via-resend.ts#L53-L61)

## 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/verify-email.tsx#L16-L52)

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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/api-key-created.tsx#L17-L87)

```tsx
export default function VerifyEmail({
  email = "panic@thedis.co",
  code = "123456",
}: {
  email: string;
  code: string;
}) {
```
Sources: [packages/email/src/templates/verify-email.tsx:16-22](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/verify-email.tsx#L16-L22)

### 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/webhook-failed.tsx#L17-L41), [packages/email/src/templates/webhook-disabled.tsx:17-39](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/webhook-disabled.tsx#L17-L39)

| 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/verify-email.tsx#L16-L22), [packages/email/src/templates/api-key-created.tsx:17-39](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/api-key-created.tsx#L17-L39), [packages/email/src/templates/webhook-failed.tsx:17-41](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/webhook-failed.tsx#L17-L41), [packages/email/src/templates/webhook-disabled.tsx:17-39](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/webhook-disabled.tsx#L17-L39)

> [!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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/webhook-failed.tsx#L55-L59), [packages/email/src/templates/webhook-disabled.tsx:53-57](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/webhook-disabled.tsx#L53-L57)

## 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/email-domain-status-changed.tsx#L17-L24)

| 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/email-domain-status-changed.tsx#L39-L53)

> [!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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/email-domain-status-changed.tsx#L39-L47)

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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/domain-renewal-reminder.tsx#L17-L43)

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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/domain-renewal-failed.tsx#L20-L67), [packages/email/src/templates/domain-expired.tsx:19-70](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/domain-expired.tsx#L19-L70), [packages/email/src/templates/domain-deleted.tsx:17-50](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/domain-deleted.tsx#L17-L50), [packages/email/src/templates/domain-transferred.tsx:18-54](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/domain-transferred.tsx#L18-L54)

## 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/partners/create-stripe-transfer.ts#L303-L321)

> [!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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/partners/create-stripe-transfer.ts#L328-L343)

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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/new-message-from-program.tsx#L24-L24), [packages/email/src/templates/new-message-from-partner.tsx:24-24](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/new-message-from-partner.tsx#L24-L24)

| 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/new-message-from-program.tsx#L76-L92), [packages/email/src/templates/new-message-from-partner.tsx:56-69](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/new-message-from-partner.tsx#L56-L69), [packages/email/src/templates/bounty-new-submission.tsx:36-54](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/bounty-new-submission.tsx#L36-L54), [packages/email/src/templates/lead-status-updated.tsx:33-44](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/lead-status-updated.tsx#L33-L44)

### 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/new-submitted-lead-comments-from-program.tsx#L62-L62), [packages/email/src/templates/new-submitted-lead-comments-from-partner.tsx:59-59](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/new-submitted-lead-comments-from-partner.tsx#L59-L59)

> [!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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/lead-status-updated.tsx#L67-L76)

## 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/campaign-email.tsx#L18-L20), [packages/email/src/templates/campaign-email.tsx:92-118](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/campaign-email.tsx#L92-L118)

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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/campaigns/%5BcampaignId%5D/preview/route.ts#L30-L34), [apps/web/app/ee/api/campaigns/campaignId/preview/route.ts:37-42](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/campaigns/%5BcampaignId%5D/preview/route.ts#L37-L42), [apps/web/app/ee/api/campaigns/campaignId/preview/route.ts:129-132](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/campaigns/%5BcampaignId%5D/preview/route.ts#L129-L132)

> [!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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/campaigns/%5BcampaignId%5D/preview/route.ts#L65-L81)

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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/broadcasts/dub-wrapped.tsx#L28-L75), [packages/email/src/templates/broadcasts/dub-wrapped.tsx:342-346](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/email/src/templates/broadcasts/dub-wrapped.tsx#L342-L346)

## 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/send-batch-email/route.ts#L13-L46)

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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/email/email-templates-map.ts#L1-L32)

> [!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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/send-batch-email/route.ts#L54-L173)

### 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/domains/email/page-client.tsx#L23-L35)

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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/domains/email/page-client.tsx#L36-L105)

## Related

- [Scheduled Cron Tasks](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/background-processing/scheduled-cron-tasks)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
