CoreMVP

Email

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:

ModeBehaviorRequired settings
realSends purchase confirmations through ResendRESEND_API_KEY, RESEND_FROM_EMAIL
test_captureCaptures guest setup mail for protected test retrieval; suppresses signed-in purchase confirmations and subscription welcome deliveryTEST_EMAIL_CAPTURE_TOKEN; guest capture also needs checkoutSessionId and e2eRunId
mockSuppresses delivery without exposing account setup linksLocal 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:

MessageService methodContent owners
Guest onboardingemailService.sendOnboardingEmail()ONBOARDING_SUBJECT, buildOnboardingHtml(), buildOnboardingText()
Subscription welcomeemailService.sendSubscriptionWelcomeEmail()SUBSCRIPTION_WELCOME_SUBJECT, buildSubscriptionWelcomeHtml(), buildSubscriptionWelcomeText()
Shared HTML layoutAll three messagesrenderEmailShell()

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:subscription

These checks do not prove inbox delivery. Verify actual sender delivery separately when intentionally using real mode.

Was this page helpful?

On this page