Email Branding
Supabase auth hooks and email branding customization
Status: Future Enhancement (Requires Supabase Team/Pro Plan) Created: 2026-02-05 Priority: Medium (implement after revenue milestone)
Overview
Supabase Auth Hooks allow intercepting and customizing authentication emails, providing full control over branding and email delivery for password changes, resets, and other auth flows.
Current Limitation: Auth Hooks require Supabase Team or Pro plan. Free tier does not support this feature.
The Problem
Current State:
- Password change confirmation emails use Supabase's plain, non-customizable template
- Only some auth emails (invite, reset password) can be branded via Dashboard templates
- Password change notifications sent via
auth.updateUser({ password })cannot be customized
Plain Email Users Currently See:
Your password has been changed
This is a confirmation that the password for your account [email] has just been changed.
If you did not make this change, please contact support.
Desired State:
- All auth emails use CritForge branding (dragon theme, colors, logo)
- Consistent email experience across signup, password reset, password change, etc.
- Full control over content, styling, and email delivery service
Solution: Send Email Hook
What It Does
The Send Email Hook intercepts ALL outgoing auth emails and allows you to:
- Customize email content and styling
- Suppress default Supabase emails
- Route to your preferred email service (Resend, SendGrid, etc.)
- Add analytics/tracking
- Implement custom logic based on email type
Email Types Intercepted
type EmailType =
| 'signup' // Email confirmation on signup
| 'invite' // User invitation
| 'magic_link' // Magic link login
| 'recovery' // Password reset request
| 'email_change' // Email change confirmation
| 'reauthentication' // Reauthentication request
| 'password_changed'; // Password change confirmation ← TARGET
Implementation Plan
Phase 1: Create Auth Hook API Endpoint
File: src/app/api/auth/hooks/send-email/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Resend } from 'resend';
import {
PasswordChangedEmail,
PasswordResetEmail,
WelcomeEmail
} from '@/lib/email/templates';
const resend = new Resend(process.env.RESEND_API_KEY);
/**
* Supabase Auth Hook: Send Email
*
* Intercepts all auth emails and sends branded versions via Resend.
*
* @see https://supabase.com/docs/guides/auth/auth-hooks
*/
export async function POST(req: NextRequest) {
try {
// Verify request is from Supabase
const authHeader = req.headers.get('authorization');
const expectedSecret = process.env.SUPABASE_AUTH_HOOK_SECRET;
if (authHeader !== `Bearer ${expectedSecret}`) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Parse auth hook event
const event = await req.json();
const {
email_type,
user,
token_hash,
redirect_to
} = event;
console.log('[AUTH_HOOK] Intercepted email:', {
type: email_type,
user_email: user.email
});
// Handle different email types
switch (email_type) {
case 'password_changed':
await sendPasswordChangedEmail(user.email);
break;
case 'recovery':
await sendPasswordResetEmail(user.email, token_hash);
break;
case 'signup':
await sendWelcomeEmail(user.email, token_hash);
break;
default:
// For other types, let Supabase handle with default templates
return NextResponse.json({ decision: 'continue' });
}
// Suppress Supabase default email (we sent custom one)
return NextResponse.json({ decision: 'suppress' });
} catch (error) {
console.error('[AUTH_HOOK] Error:', error);
// On error, let Supabase send default email (failsafe)
return NextResponse.json({ decision: 'continue' });
}
}
async function sendPasswordChangedEmail(email: string) {
await resend.emails.send({
from: 'CritForge [email protected]',
to: email,
subject: 'Password Changed Successfully - CritForge',
html: PasswordChangedEmail({ email }),
});
}
async function sendPasswordResetEmail(email: string, tokenHash: string) {
const resetUrl = `${process.env.NEXT_PUBLIC_SITE_URL}/update-password?token_hash=${tokenHash}&type=recovery`;
await resend.emails.send({
from: 'CritForge [email protected]',
to: email,
subject: 'Reset Your Password - CritForge',
html: PasswordResetEmail({ email, resetUrl }),
});
}
async function sendWelcomeEmail(email: string, tokenHash: string) {
const confirmUrl = `${process.env.NEXT_PUBLIC_SITE_URL}/auth/confirm?token_hash=${tokenHash}&type=email`;
await resend.emails.send({
from: 'CritForge [email protected]',
to: email,
subject: 'Welcome to CritForge!',
html: WelcomeEmail({ email, confirmUrl }),
});
}
Phase 2: Create Email Templates
File: src/lib/email/templates/password-changed-email.tsx
interface PasswordChangedEmailProps {
email: string;
}
export function PasswordChangedEmail({ email }: PasswordChangedEmailProps) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Password Changed - CritForge</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5ede0;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f5ede0; padding: 40px 20px;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #fdf9f3; border-radius: 8px; box-shadow: 0 4px 6px rgba(74, 64, 51, 0.15); border: 2px solid #d4a84b;">
<!-- Header -->
<tr>
<td style="background-color: #4a4033; padding: 40px 40px 30px; border-radius: 6px 6px 0 0; text-align: center; border-bottom: 3px solid #b8860b;">
<h1 style="margin: 0; color: #d4a84b; font-size: 32px; font-weight: 700; font-family: 'Cinzel', 'Georgia', serif; text-shadow: 0 2px 4px rgba(0,0,0,0.3);">
CritForge
</h1>
<p style="margin: 10px 0 0; color: #f5ede0; font-size: 14px; letter-spacing: 1px;">
AI-ASSISTED D&D CONTENT GENERATION
</p>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px; background-color: #fdf9f3;">
<h2 style="margin: 0 0 20px; color: #4a4033; font-size: 24px; font-weight: 600; font-family: 'Cinzel', 'Georgia', serif;">
Password Changed Successfully ✓
</h2>
<p style="margin: 0 0 20px; color: #4b5563; font-size: 16px; line-height: 1.6;">
This confirms that the password for your CritForge account <strong>${email}</strong> has been changed.
</p>
<p style="margin: 0 0 20px; color: #4b5563; font-size: 16px; line-height: 1.6;">
Your account is secure and you can continue using CritForge with your new password.
</p>
<!-- Security Notice -->
<div style="margin-top: 30px; padding: 16px; background-color: #fef3c7; border-left: 4px solid #f59e0b; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
<p style="margin: 0 0 8px; color: #4a4033; font-size: 14px;">
<strong>⚠️ Didn't make this change?</strong>
</p>
<p style="margin: 0; color: #4b5563; font-size: 13px;">
If you didn't request this password change, please contact us immediately at
<a href="mailto:[email protected]" style="color: #8b0000; text-decoration: none; font-weight: 600;">[email protected]</a>
</p>
</div>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding: 30px 40px; background-color: #f5ede0; border-radius: 0 0 6px 6px; border-top: 2px solid #d4c4a8;">
<p style="margin: 0; color: #6b7280; font-size: 12px; text-align: center;">
© 2026 CritForge. All rights reserved.<br>
Questions? Email us at <a href="mailto:[email protected]" style="color: #8b0000; text-decoration: none;">[email protected]</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`;
}
Phase 3: Configure in Supabase Dashboard
Prerequisites:
- ✅ Supabase Team or Pro plan subscription
- ✅ Resend account with API key
- ✅ Custom domain configured in Resend
Steps:
-
Generate Auth Hook Secret
# Generate random secret for hook authentication openssl rand -base64 32Add to
.env.local:SUPABASE_AUTH_HOOK_SECRET=your_generated_secret -
Deploy Hook Endpoint
- Ensure
/api/auth/hooks/send-emailis deployed to production - Test endpoint is accessible:
https://getcritforge.com/api/auth/hooks/send-email
- Ensure
-
Configure Hook in Supabase
- Go to Supabase Dashboard → Authentication → Hooks
- Click Add hook → Send Email
- Select HTTP Endpoint
- Enter:
- URL:
https://getcritforge.com/api/auth/hooks/send-email - HTTP Method:
POST - Authorization:
Bearer [your_generated_secret]
- URL:
- Click Create
-
Test the Hook
- Change your password in Settings
- Check email for branded template
- Verify Supabase logs show hook execution
Environment Variables Required
# Resend API Key (for sending emails)
RESEND_API_KEY=re_xxx
# Auth Hook Secret (for verifying requests from Supabase)
SUPABASE_AUTH_HOOK_SECRET=your_generated_secret
# Site URL (for constructing links in emails)
NEXT_PUBLIC_SITE_URL=https://getcritforge.com
Benefits
✅ Full Branding Control: All auth emails match CritForge design ✅ Consistent UX: No more plain Supabase emails ✅ Email Service Choice: Use Resend, SendGrid, or any provider ✅ Analytics Ready: Track email opens, clicks, conversions ✅ Customizable Logic: Add business rules per email type ✅ Failsafe: Falls back to Supabase defaults on errors
Cost Analysis
Supabase Plan Upgrade:
- Team: $25/month (includes Auth Hooks)
- Pro: $599/month (overkill for this feature alone)
Recommendation: Upgrade to Team plan when monthly revenue > $250/month (10x ROI)
Additional Costs:
- Resend: Free tier = 3,000 emails/month (likely sufficient at start)
- After 3,000: $0.001/email ($10 per 10,000 emails)
Break-Even Analysis:
Monthly Revenue Threshold: $250
Auth Hook emails sent: ~500/month (password changes, resets, signups)
Additional Resend cost: ~$0 (within free tier)
Net cost: $25/month for Supabase Team
ROI: Wait until $250+/month revenue before implementing
Alternative (Current): Accept Plain Emails
For Now:
- ✅ Invite emails: Branded via Supabase templates
- ✅ Password reset: Branded via Supabase templates
- ❌ Password change confirmation: Plain Supabase email
Workaround: Disable password change notifications (if possible) and only send on security-critical events.
Implementation Checklist (When Ready)
Prerequisites
- Monthly revenue > $250
- Supabase Team plan subscribed
- Resend account created and verified
- Custom domain configured in Resend
Development
- Create
/api/auth/hooks/send-emailendpoint - Create email templates (password changed, reset, welcome)
- Add environment variables
- Test locally with ngrok/Supabase local
Deployment
- Deploy to production
- Generate auth hook secret
- Configure hook in Supabase Dashboard
- Test password change flow
- Monitor Supabase logs for hook execution
- Verify no plain emails sent
Monitoring
- Set up Resend analytics
- Track email delivery rates
- Monitor for hook failures
- Add logging/alerts for errors
Resources
- Supabase Auth Hooks Docs
- Supabase Pricing - Auth Hooks on Team/Pro
- Resend Quickstart
- Email Template Best Practices
Notes
- Auth Hooks are a BETA feature as of 2026-02-05
- Monitor Supabase changelog for GA release and potential pricing changes
- Consider implementing after hitting revenue milestone to justify cost
- Template design can be reused across all auth emails for consistency
Status: Documented for future implementation when revenue supports Team plan upgrade.