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
- Go to Supabase Dashboard → Authentication → Email Templates
- Customize the templates for each email type
- Use variables like
{{ .ConfirmationURL }}for dynamic content
SMTP Configuration
For production, configure custom SMTP in Supabase:
- Go to Project Settings → Auth → SMTP Settings
- Enter your SMTP provider details (Resend, SendGrid, etc.)
- This allows emails to come from your domain
Adding Transactional Emails (Optional)
For custom emails (welcome, purchase confirmation), add Resend.
Install Resend
bun add resendCreate Email Service
Create a server-only email service:
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/componentsCreate email components:
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:
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({
/* ... */
});
}Related
- Authentication - Email verification
- Payments - Purchase confirmations
Was this page helpful?