Configure Supabase Auth mail and the included transactional email service.
Supabase Auth sends authentication mail, including verification, magic links, and password recovery. The application's email service sends eligible purchase confirmations to guest and signed-in buyers through Resend. Configure these two delivery paths separately.
Configure authentication mail
Use Supabase's custom SMTP guide to configure a sender for hosted users. The built-in SMTP service has delivery restrictions and is unsuitable for production delivery.
Follow Supabase's email template guide
to customize hosted templates in the Dashboard or local templates through
supabase/config.toml. Preserve the confirmation and recovery links required by
each flow. New Free projects using default SMTP cannot customize hosted Auth
templates; configure custom SMTP first. Existing projects may retain access under
Supabase's template customization policy.
Configure transactional delivery
The included src/services/email/service.ts exports the emailService
singleton. It calls Resend's HTTP API directly; the shipped path needs no Resend
SDK or React Email package.
Choose EMAIL_DELIVERY_MODE in your ignored .env.local for local work, or in
your hosting provider's server environment for deployment:
| Mode | Behavior | Required settings |
|---|---|---|
real | Sends purchase confirmations through Resend | RESEND_API_KEY, RESEND_FROM_EMAIL |
test_capture | Captures guest setup mail for protected test retrieval; suppresses signed-in purchase confirmations and subscription welcome delivery | TEST_EMAIL_CAPTURE_TOKEN; guest capture also needs checkoutSessionId and e2eRunId |
mock | Suppresses delivery without exposing account setup links | Local development only |
For real delivery, verify a domain in Resend
and set RESEND_FROM_EMAIL to a sender on that domain. Keep the API key and test
capture token server-side. Real mode sends real email, including when run locally.
At runtime, hosted execution (VERCEL=1 or NODE_ENV=production) requires an
explicit real or test_capture mode. The service validates the chosen mode
and its required settings when it handles a send; hosted mock is rejected.
Building the application alone does not verify delivery configuration. Normal
local development can set EMAIL_DELIVERY_MODE=mock.
See Environment Variables for the complete settings.
Customize purchase confirmations
For the current purchase confirmation, edit buildPurchaseConfirmation() in
src/services/email/service.ts. It builds the subject, HTML and plain text;
emailService.sendPurchaseConfirmationEmail() sends them using the configured
sender and recipient. Change both versions and preserve the guest password setup
link, Billing link and protected-content link as appropriate. The HTML uses
renderEmailShell(), which is shared with the specialized messages below.
Keep message construction and delivery settings server-side.
Billing sends confirmations for eligible new lifetime and initial subscription
purchases. Stripe Checkout and invoice.paid, and Polar's verified order.paid,
use the original checkout's durable delivery record to suppress duplicates. The
first successful payment after a trial counts as the initial purchase; renewals,
refunds and an unfunded trial start do not send another confirmation. Guest
lifetime checkout includes its saved password setup link; signed-in buyers get
Billing and protected-content links without a password action. Demo messages
identify test purchases and state that no real charge was made.
Keep purchase eligibility in the billing services. The email service owns the message and delivery envelope. Do not log or expose password setup links.
Extend the specialized messages
Older checkout paths still use these builders in src/services/email/service.ts:
| Message | Service method | Content owners |
|---|---|---|
| Guest onboarding | emailService.sendOnboardingEmail() | ONBOARDING_SUBJECT, buildOnboardingHtml(), buildOnboardingText() |
| Subscription welcome | emailService.sendSubscriptionWelcomeEmail() | SUBSCRIPTION_WELCOME_SUBJECT, buildSubscriptionWelcomeHtml(), buildSubscriptionWelcomeText() |
| Shared HTML layout | All three messages | renderEmailShell() |
For checkout records without the current purchase email contract, Stripe sends
the subscription welcome on subscription checkout completion and sends guest
onboarding when the resolved checkout user needs it. Polar also uses guest
onboarding for eligible lifetime checkouts without that contract. When adapting
these messages, change HTML and plain text together. Preserve the onboarding
passwordSetupLink and any GitHub invitation link your application uses. The
subscription welcome accepts a dashboardLink.
Preserve webhook delivery ownership
Verified Stripe events are handled in src/services/billing/webhook.ts;
verified Polar events use the Polar billing handlers. Both use the existing
email service for eligible purchase confirmations. Extend the corresponding
provider path when changing purchase mail.
For guest onboarding, OnboardingEmailDeliveryRepository stores delivery state
and the password setup link, claims a delivery before sending, and records
success or failure. The service receives the stored idempotency key; real mode
passes it to Resend. Repeated event processing reuses this state and key.
Stripe subscription welcome delivery uses an idempotency key derived from the
Checkout session ID. A failed email result propagates through webhook processing
rather than being silently treated as delivered. Provider acceptance does not establish
that a message reached the inbox.
Keep the Hono webhook route responsible for raw-body signature verification and HTTP responses. Adding a second direct send there, or replacing the Auth signup flow with a custom welcome sender, would bypass the existing delivery rules. Supabase Auth remains responsible for authentication email.
Test without sending mail
Use mock for ordinary local development. Use test_capture when the billing
E2E needs to retrieve the guest password setup link without delivering to an
inbox. The billing checkout mutation accepts an e2eRunId, which the webhook
passes into the existing capture path. The capture token protects test retrieval;
it belongs in server/test settings, never public build inputs.
The existing local billing suites cover the purchase paths:
./coremvp e2e billing:lifetime
./coremvp e2e billing:subscriptionThese checks do not prove inbox delivery. Verify actual sender delivery
separately when intentionally using real mode.
Related
- Authentication - Auth email flows
- Payments - Checkout and webhook owners
Was this page helpful?