CoreMVP

Email

Email functionality with Supabase Auth and optional Resend integration.

Overview

Email in this template is handled in two ways:

  • Supabase Auth emails - Verification and magic links (built-in)
  • Transactional emails - Custom emails via Resend (optional)

Supabase Auth Emails

Supabase Auth automatically sends emails for:

  • Email verification on signup
  • Magic link sign-in

Customizing Auth Email Templates

  1. Go to Supabase Dashboard → Authentication → Email Templates
  2. Customize the templates for each email type
  3. Use variables like {{ .ConfirmationURL }} for dynamic content

SMTP Configuration

For production, configure custom SMTP in Supabase:

  1. Go to Project Settings → Auth → SMTP Settings
  2. Enter your SMTP provider details (Resend, SendGrid, etc.)
  3. This allows emails to come from your domain

Adding Transactional Emails (Optional)

For custom emails (welcome, purchase confirmation), add Resend.

Install Resend

bun add resend

Add API Key

Add to .env.local:

RESEND_API_KEY=re_...

Create Email Service

Create a server-only email service:

src/services/email/service.ts
import 'server-only';

import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

export async function sendWelcomeEmail(to: string, name: string) {
  await resend.emails.send({
    from: 'noreply@yourdomain.com',
    to,
    subject: 'Welcome!',
    html: `<h1>Welcome, ${name}!</h1><p>Thanks for signing up.</p>`,
  });
}

export async function sendPurchaseConfirmation(to: string, amount: number) {
  await resend.emails.send({
    from: 'noreply@yourdomain.com',
    to,
    subject: 'Purchase Confirmed',
    html: `<p>Thank you for your purchase of $${amount}!</p>`,
  });
}

Verify Domain

Add DNS records in Resend dashboard for your sending domain.

Email Templates

For complex templates, use React Email:

bun add @react-email/components

Create email components:

src/services/email/templates/welcome.tsx
import { Html, Head, Body, Container, Heading, Text, Link } from '@react-email/components';

export function WelcomeEmail({ name }: { name: string }) {
  return (
    <Html>
      <Head />
      <Body>
        <Container>
          <Heading>Welcome, {name}!</Heading>
          <Text>We're excited to have you on board.</Text>
          <Link href="https://yourdomain.com/dashboard">Get Started</Link>
        </Container>
      </Body>
    </Html>
  );
}

Render and send:

import { render } from '@react-email/render';
import { WelcomeEmail } from './templates/welcome';

const html = render(<WelcomeEmail name={name} />);

await resend.emails.send({
  from: 'noreply@yourdomain.com',
  to,
  subject: 'Welcome!',
  html,
});

Sending on Events

After Signup

Call from the auth service or a database trigger:

src/services/auth/service.ts
export async function signUp({ email, password }: SignUpParams) {
  const supabase = await createSupabaseServerClient();
  const { data, error } = await supabase.auth.signUp({ email, password });

  if (data.user) {
    await sendWelcomeEmail(email, email.split('@')[0]);
  }

  return { ok: true, redirectPath: '/' };
}

After Purchase

Billing already sends one confirmation for each new successful lifetime or initial subscription purchase through the server-only email service. Configure EMAIL_DELIVERY_MODE=real, RESEND_API_KEY, and RESEND_FROM_EMAIL to deliver these messages. Use an inbox you control when testing real delivery.

Stripe Checkout and invoice.paid, and Polar's verified order.paid, use the original checkout's durable delivery record to suppress duplicate messages. 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 in that same message. Signed-in buyers receive Billing and protected-content links without a password action. The public demo explicitly labels test purchases and real charges are not made.

Keep Hono webhook routes responsible for raw-body signature verification and HTTP responses. Customize confirmation HTML/text in src/services/email/service.ts; keep purchase eligibility in the billing services. test_capture preserves guest setup email for its protected retrieval endpoint; signed-in confirmations are suppressed in non-real modes. Capture and suppression are not inbox delivery.

Testing

Resend free tier: 100 emails/day, 3,000/month. Sufficient for development.

For local testing without sending real emails:

export async function sendWelcomeEmail(to: string, name: string) {
  if (process.env.NODE_ENV === 'development') {
    console.log(`[EMAIL] Welcome email to ${to}: Hello ${name}`);
    return;
  }
  
  await resend.emails.send({
    /* ... */
  });
}

Was this page helpful?

On this page