Supabase Email Templates: Custom Transactional Emails

Customizing email templates enables branded transactional emails for authentication, password resets, notifications, and user engagement maintaining consistent design, personalizing content with user data, supporting multiple languages, and enhancing user experience through professional communication. Unlike generic default emails lacking branding and context, custom templates reinforce brand identity, improve email deliverability with proper formatting, increase user trust through professional appearance, and provide contextual information guiding users through actions. This comprehensive guide covers understanding Supabase email types and customization options, configuring SMTP settings for custom email provider integration, designing HTML email templates with responsive layouts, customizing authentication emails for sign up verification and password reset, implementing dynamic content with template variables, adding localization for multi-language support, testing email deliverability and spam scores, and integrating third-party email services like SendGrid Mailgun and Postmark. Email customization becomes essential when building branded products, sending marketing communications, supporting international users, improving conversion rates, or maintaining professional communication standards. Before proceeding, understand authentication, Edge Functions, and webhooks.
Supabase Email Types
| Email Type | Trigger | Customizable |
|---|---|---|
| Confirmation Email | User signs up | Yes |
| Magic Link | Passwordless login | Yes |
| Password Recovery | Reset password request | Yes |
| Email Change | User updates email | Yes |
| Invite Email | Team invitation | Yes |
SMTP Configuration
# Configure Custom SMTP in Supabase Dashboard
# Go to: Authentication > Settings > SMTP Settings
# Enable Custom SMTP
# Sender email: [email protected]
# Sender name: Your App Name
# SMTP Configuration Examples:
# SendGrid
Host: smtp.sendgrid.net
Port: 587
Username: apikey
Password: YOUR_SENDGRID_API_KEY
# Mailgun
Host: smtp.mailgun.org
Port: 587
Username: [email protected]
Password: YOUR_MAILGUN_PASSWORD
# AWS SES
Host: email-smtp.us-east-1.amazonaws.com
Port: 587
Username: YOUR_AWS_ACCESS_KEY
Password: YOUR_AWS_SECRET_KEY
# Postmark
Host: smtp.postmarkapp.com
Port: 587
Username: YOUR_POSTMARK_SERVER_TOKEN
Password: YOUR_POSTMARK_SERVER_TOKEN
# Gmail (for testing only - not recommended for production)
Host: smtp.gmail.com
Port: 587
Username: [email protected]
Password: your-app-password
# Test SMTP configuration
curl -X POST https://your-project.supabase.co/auth/v1/admin/generate_link \
-H "apikey: YOUR_SERVICE_ROLE_KEY" \
-H "Authorization: Bearer YOUR_SERVICE_ROLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "signup",
"email": "[email protected]"
}'
# Verify SPF and DKIM records for your domain
# Add to DNS:
# SPF: v=spf1 include:_spf.youremailprovider.com ~all
# DKIM: Provided by your email service
# DMARC: v=DMARC1; p=none; rua=mailto:[email protected]Authentication Email Templates
<!-- Confirmation Email Template -->
<!-- Go to: Authentication > Email Templates > Confirm signup -->
<html>
<head>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
padding: 20px 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px 8px 0 0;
}
.content {
padding: 30px;
background: #ffffff;
border: 1px solid #e0e0e0;
}
.button {
display: inline-block;
padding: 12px 30px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 6px;
margin: 20px 0;
font-weight: 600;
}
.footer {
text-align: center;
padding: 20px;
color: #666;
font-size: 12px;
}
</style>
</head>
<body>
<div class="header">
<h1>Welcome to {{ .SiteURL }}</h1>
</div>
<div class="content">
<h2>Confirm your email address</h2>
<p>Hi {{ .Email }},</p>
<p>Thanks for signing up! Please click the button below to confirm your email address and activate your account.</p>
<a href="{{ .ConfirmationURL }}" class="button">
Confirm Email Address
</a>
<p>Or copy and paste this URL into your browser:</p>
<p style="word-break: break-all; color: #666;">{{ .ConfirmationURL }}</p>
<p>This link will expire in 24 hours.</p>
<p>If you didn't create an account, you can safely ignore this email.</p>
</div>
<div class="footer">
<p>© 2026 Your Company. All rights reserved.</p>
<p>123 Main St, City, Country</p>
</div>
</body>
</html>
<!-- Magic Link Template -->
<!-- Go to: Authentication > Email Templates > Magic Link -->
<html>
<head>
<style>
/* Same styles as above */
</style>
</head>
<body>
<div class="header">
<h1>🔐 Your Magic Link</h1>
</div>
<div class="content">
<h2>Sign in to {{ .SiteURL }}</h2>
<p>Hi {{ .Email }},</p>
<p>Click the button below to sign in to your account. No password needed!</p>
<a href="{{ .ConfirmationURL }}" class="button">
Sign In Now
</a>
<p style="word-break: break-all; color: #666;">{{ .ConfirmationURL }}</p>
<p>This link will expire in 60 minutes and can only be used once.</p>
<p><strong>Security Note:</strong> If you didn't request this link, please ignore this email.</p>
</div>
<div class="footer">
<p>Sent by {{ .SiteURL }}</p>
</div>
</body>
</html>
<!-- Password Recovery Template -->
<!-- Go to: Authentication > Email Templates > Reset Password -->
<html>
<head>
<style>
/* Same styles as above */
</style>
</head>
<body>
<div class="header">
<h1>Reset Your Password</h1>
</div>
<div class="content">
<h2>Password Reset Request</h2>
<p>Hi {{ .Email }},</p>
<p>We received a request to reset your password. Click the button below to create a new password.</p>
<a href="{{ .ConfirmationURL }}" class="button">
Reset Password
</a>
<p style="word-break: break-all; color: #666;">{{ .ConfirmationURL }}</p>
<p>This link will expire in 1 hour.</p>
<p><strong>Didn't request this?</strong> If you didn't request a password reset, please ignore this email or contact support if you have concerns.</p>
</div>
<div class="footer">
<p>For security reasons, this link can only be used once.</p>
</div>
</body>
</html>
<!-- Available Template Variables -->
<!-- {{ .Email }} - User's email address -->
<!-- {{ .ConfirmationURL }} - Confirmation/action URL -->
<!-- {{ .Token }} - Verification token -->
<!-- {{ .TokenHash }} - Hashed token -->
<!-- {{ .SiteURL }} - Your site URL -->
<!-- {{ .RedirectTo }} - Redirect URL after confirmation -->Custom Transactional Emails
// Send custom emails with Edge Functions
// supabase/functions/send-email/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
interface EmailPayload {
to: string
subject: string
templateName: string
variables: Record<string, string>
}
serve(async (req) => {
const { to, subject, templateName, variables }: EmailPayload = await req.json()
// Load template
const template = await loadTemplate(templateName)
const html = renderTemplate(template, variables)
// Send via SendGrid
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('SENDGRID_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{
to: [{ email: to }],
subject,
}],
from: {
email: '[email protected]',
name: 'Your App',
},
content: [{
type: 'text/html',
value: html,
}],
}),
})
if (!response.ok) {
throw new Error('Failed to send email')
}
return new Response(
JSON.stringify({ success: true }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
function loadTemplate(name: string): string {
const templates: Record<string, string> = {
welcome: `
<h1>Welcome, {{name}}!</h1>
<p>Thanks for joining {{appName}}.</p>
`,
notification: `
<h2>{{title}}</h2>
<p>{{message}}</p>
`,
invoice: `
<h2>Invoice #{{invoiceNumber}}</h2>
<p>Amount: {{amount}}</p>
<p>Due Date: {{dueDate}}</p>
`,
}
return templates[name] || ''
}
function renderTemplate(template: string, variables: Record<string, string>): string {
let html = template
for (const [key, value] of Object.entries(variables)) {
html = html.replace(new RegExp(`{{${key}}}`, 'g'), value)
}
return html
}
// Usage from application
import { supabase } from './lib/supabase'
async function sendWelcomeEmail(user: any) {
const { data, error } = await supabase.functions.invoke('send-email', {
body: {
to: user.email,
subject: 'Welcome to Our App!',
templateName: 'welcome',
variables: {
name: user.name,
appName: 'Your App',
},
},
})
if (error) {
console.error('Failed to send email:', error)
}
}
// HTML Email Template with Inline CSS
const emailTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{subject}}</title>
</head>
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f4f4f4;">
<table role="presentation" style="width: 100%; border-collapse: collapse;">
<tr>
<td align="center" style="padding: 40px 0;">
<table role="presentation" style="width: 600px; border-collapse: collapse; background-color: #ffffff; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="padding: 40px 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px;">{{companyName}}</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px 30px;">
<h2 style="margin-top: 0; color: #333333;">{{title}}</h2>
<p style="color: #666666; line-height: 1.6; margin-bottom: 20px;">
{{content}}
</p>
<!-- CTA Button -->
<table role="presentation" style="margin: 30px 0;">
<tr>
<td align="center">
<a href="{{ctaUrl}}" style="display: inline-block; padding: 15px 40px; background-color: #667eea; color: #ffffff; text-decoration: none; border-radius: 5px; font-weight: bold;">
{{ctaText}}
</a>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding: 30px; background-color: #f8f8f8; text-align: center; border-top: 1px solid #e0e0e0;">
<p style="margin: 0; color: #999999; font-size: 12px;">
© 2026 {{companyName}}. All rights reserved.
</p>
<p style="margin: 10px 0 0 0; color: #999999; font-size: 12px;">
<a href="{{unsubscribeUrl}}" style="color: #667eea; text-decoration: none;">Unsubscribe</a> |
<a href="{{preferencesUrl}}" style="color: #667eea; text-decoration: none;">Email Preferences</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`Email Testing and Deliverability
// Test email delivery
// supabase/functions/test-email/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
const { email } = await req.json()
// Send test confirmation email
const { data, error } = await supabase.auth.admin.generateLink({
type: 'signup',
email,
})
if (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500 }
)
}
return new Response(
JSON.stringify({
success: true,
message: 'Test email sent',
link: data.properties.action_link,
})
)
})
# Test with Mail-Tester
# Send test email to: test-xxx@mail-tester.com
# Check score at: https://www.mail-tester.com/test-xxx
# Monitor email deliverability
curl -X GET https://api.sendgrid.com/v3/stats \
-H "Authorization: Bearer $SENDGRID_API_KEY" \
-H "Content-Type: application/json"
# Check bounce rate
const { data: bounces } = await supabase
.from('email_logs')
.select('*')
.eq('status', 'bounced')
.gte('created_at', new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString())
const bounceRate = (bounces.length / totalSent) * 100
console.log(`Bounce rate: ${bounceRate}%`)
# Track email opens and clicks
<!-- Add tracking pixel to email HTML -->
<img src="https://your-app.com/api/email-open?id={{emailId}}" width="1" height="1" style="display:none;" />
<!-- Track link clicks -->
<a href="https://your-app.com/api/track-click?url={{encodedUrl}}&id={{emailId}}" style="color: #667eea;">
Click here
</a>
// API endpoint to track opens
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
const url = new URL(req.url)
const emailId = url.searchParams.get('id')
if (emailId) {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
await supabase
.from('email_analytics')
.update({ opened_at: new Date().toISOString() })
.eq('id', emailId)
}
// Return 1x1 transparent GIF
return new Response(
atob('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'),
{
headers: {
'Content-Type': 'image/gif',
'Cache-Control': 'no-cache',
},
}
)
})
# Email validation before sending
function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
# Check if email domain exists
async function verifyEmailDomain(email: string): Promise<boolean> {
const domain = email.split('@')[1]
try {
const response = await fetch(`https://dns.google/resolve?name=${domain}&type=MX`)
const data = await response.json()
return data.Answer && data.Answer.length > 0
} catch {
return false
}
}Multi-Language Email Templates
// Multi-language email templates
const emailTranslations = {
en: {
welcome: {
subject: 'Welcome to {{appName}}!',
title: 'Welcome aboard!',
content: 'Thanks for signing up. We\'re excited to have you.',
cta: 'Get Started',
},
passwordReset: {
subject: 'Reset your password',
title: 'Password Reset Request',
content: 'Click the button below to reset your password.',
cta: 'Reset Password',
},
},
es: {
welcome: {
subject: '¡Bienvenido a {{appName}}!',
title: '¡Bienvenido a bordo!',
content: 'Gracias por registrarte. Estamos emocionados de tenerte.',
cta: 'Comenzar',
},
passwordReset: {
subject: 'Restablecer contraseña',
title: 'Solicitud de restablecimiento de contraseña',
content: 'Haz clic en el botón para restablecer tu contraseña.',
cta: 'Restablecer Contraseña',
},
},
fr: {
welcome: {
subject: 'Bienvenue sur {{appName}}!',
title: 'Bienvenue à bord!',
content: 'Merci de vous être inscrit. Nous sommes ravis de vous avoir.',
cta: 'Commencer',
},
passwordReset: {
subject: 'Réinitialiser le mot de passe',
title: 'Demande de réinitialisation du mot de passe',
content: 'Cliquez sur le bouton pour réinitialiser votre mot de passe.',
cta: 'Réinitialiser le mot de passe',
},
},
}
function getEmailContent(
language: string,
templateName: string,
variables: Record<string, string>
): any {
const lang = emailTranslations[language as keyof typeof emailTranslations] || emailTranslations.en
const template = lang[templateName as keyof typeof lang]
return {
subject: replaceVariables(template.subject, variables),
title: template.title,
content: template.content,
cta: template.cta,
}
}
function replaceVariables(text: string, variables: Record<string, string>): string {
let result = text
for (const [key, value] of Object.entries(variables)) {
result = result.replace(new RegExp(`{{${key}}}`, 'g'), value)
}
return result
}
// Usage
import { supabase } from './lib/supabase'
async function sendLocalizedEmail(userId: string, templateName: string) {
// Get user's preferred language from profile
const { data: profile } = await supabase
.from('profiles')
.select('language, email')
.eq('id', userId)
.single()
const language = profile?.language || 'en'
const content = getEmailContent(language, templateName, {
appName: 'Your App',
})
await supabase.functions.invoke('send-email', {
body: {
to: profile?.email,
subject: content.subject,
html: renderEmailTemplate(content),
},
})
}
// Store user language preference
const { error } = await supabase
.from('profiles')
.update({ language: 'es' })
.eq('id', userId)Email Best Practices
- Use Inline CSS: Email clients don't support external stylesheets, inline all styles
- Include Plain Text Version: Always provide text alternative for email clients blocking HTML
- Test Across Clients: Verify rendering in Gmail, Outlook, Apple Mail, and mobile clients
- Optimize Images: Keep file sizes small and provide alt text for accessibility
- Authenticate Domain: Configure SPF, DKIM, and DMARC records preventing spam classification
- Monitor Deliverability: Track bounce rates, open rates, and spam complaints adjusting strategy
- Provide Unsubscribe: Include clear unsubscribe link complying with CAN-SPAM and GDPR
Common Email Issues
- Emails Not Sending: Verify SMTP credentials, check DNS records (SPF/DKIM), and review provider quotas
- Landing in Spam: Configure domain authentication, avoid spam trigger words, maintain clean mailing list
- Template Variables Not Replacing: Check syntax matches provider format ({{ }} vs %%), verify variable names
- Broken Layout on Mobile: Use responsive design, test with real devices, use max-width: 600px for containers
Next Steps
- Automate Workflows: Combine with webhooks for triggered emails
- Build Functions: Create Edge Functions for custom logic
- Enhance Auth: Improve authentication flows
- Track Analytics: Monitor with analytics
Conclusion
Customizing email templates enables branded transactional emails maintaining consistent design, personalizing content with user data, supporting multiple languages, and enhancing user experience through professional communication. By configuring SMTP settings integrating custom email providers like SendGrid Mailgun or Postmark, designing HTML templates with responsive layouts and inline CSS, customizing authentication emails for sign up verification magic links and password resets, implementing dynamic content replacing template variables, adding localization supporting multiple languages with user preferences, testing deliverability across email clients and spam filters, and monitoring email performance tracking open rates bounce rates and click-through rates, you build professional email communication. Email customization advantages include reinforcing brand identity through consistent design, improving deliverability with proper authentication, increasing user trust through professional appearance, providing contextual information guiding users, supporting international audiences with localization, and maintaining compliance with CAN-SPAM and GDPR regulations. Always use inline CSS for compatibility, include plain text alternatives, test across email clients Gmail Outlook and mobile, optimize images keeping sizes small, authenticate domain with SPF DKIM and DMARC, monitor deliverability metrics, and provide clear unsubscribe options. Email customization becomes essential when building branded products, sending marketing communications, supporting international users, improving conversion rates, or maintaining professional standards. Continue with webhooks, Edge Functions, and security.
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


