BLOGS
Custom Email Notification Templates in Supabase (2026)
How to customize Supabase auth email templates, from built-in variables and custom metadata to Go template conditionals and the Send Email Hook for full control.
Motivation
Supabase ships six authentication email templates out of the box. They work, but the default HTML is bare. No branding, no localization, no way to send different content based on who the user is.
There are typical scenarios that require customization, such as:
- Customization based on the type of user, e.g. buyers vs. sellers
- Localization based on the user’s language or locale
- Customization based on the user’s plan, e.g. free vs. pro
This guide covers four levels of customization to address these:
- Dashboard templates with built-in variables
- Dashboard templates with custom variables - such as
{{ .Data.first_name }} - Dashboard templates with logic - with Go template conditionals
- Custom templates - bypass Supabase’s email system entirely
Approach 1: Dashboard Templates with Built-in Variables
The simplest approach. In the Supabase dashboard, go to Authentication > Email Templates and edit the HTML directly.
Available variables
Every template has access to these Go template variables:
| Variable | Description | Example |
|---|---|---|
{{ .ConfirmationURL }} | The full verification link | https://abc.supabase.co/auth/... |
{{ .Token }} | 6-digit OTP code | 482910 |
{{ .TokenHash }} | Hashed version of the token | a1b2c3d4e5f6... |
{{ .SiteURL }} | Your app’s site URL | https://myapp.com |
{{ .RedirectTo }} | Redirect URL passed from the client | https://myapp.com/welcome |
{{ .Email }} | The user’s email address | maria@example.com |
For email change templates, you also get {{ .NewEmail }} and {{ .OldEmail }}.
Example: signup confirmation
<h2>Confirm your email</h2>
<p>Hey {{ .Email }},</p>
<p>Click below to confirm your account:</p>
<a href="{{ .ConfirmationURL }}">Confirm Email</a>
<p>Or enter this code manually: <strong>{{ .Token }}</strong></p>
Approach 2: Dashboard Templates with Custom Variables
You can pass custom metadata when a user signs up, then reference those fields in your templates via {{ .Data }}.
Passing metadata at signup
When a user signs up, include metadata that your templates can reference:
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'securepassword',
options: {
data: {
role: 'seller',
language: 'es',
first_name: 'Maria',
plan: 'pro'
}
}
});
All fields under data are accessible via {{ .Data.role }}, {{ .Data.language }}, {{ .Data.first_name }}, etc. in your templates.
Users can also update their metadata later, and future auth emails will reflect the updated values:
const { data, error } = await supabase.auth.updateUser({
data: { language: 'fr', first_name: 'Marie' }
});
Example: personalized signup confirmation
<h2>Welcome, {{ .Data.first_name }}!</h2>
<p>Thanks for signing up for the {{ .Data.plan }} plan.</p>
<p>Click below to confirm your account:</p>
<a href="{{ .ConfirmationURL }}">Confirm Email</a>
<p>Or enter this code manually: <strong>{{ .Token }}</strong></p>
Approach 3: Dashboard Templates with Logic
Since Supabase uses Go’s html/template engine, you can use conditionals and comparisons to render different content per user.
Example: different emails per user role
A common need, e.g. send sellers a different confirmation than buyers:
{{ if eq .Data.role "seller" }}
<h2>Welcome, Seller!</h2>
<p>Hi {{ .Data.first_name }},</p>
<p>Confirm your seller account to start listing products:</p>
<a href="{{ .ConfirmationURL }}">Activate Seller Account</a>
<p>
After confirming, you'll be able to set up your storefront and payout details.
</p>
{{ else if eq .Data.role "buyer" }}
<h2>Welcome!</h2>
<p>Hi {{ .Data.first_name }},</p>
<p>Confirm your email to start shopping:</p>
<a href="{{ .ConfirmationURL }}">Confirm Email</a>
{{ else }}
<h2>Confirm your email</h2>
<p>Follow this link: <a href="{{ .ConfirmationURL }}">Confirm</a></p>
{{ end }}
Example: localized emails based on user language
Store a language preference in metadata, then conditionally render different content:
{{ if eq .Data.language "es" }}
<h2>¡Bienvenido!</h2>
<p>Haz clic aquí para confirmar tu correo electrónico:</p>
<a href="{{ .ConfirmationURL }}">Confirmar correo</a>
<p>O ingresa este código: <strong>{{ .Token }}</strong></p>
{{ else if eq .Data.language "fr" }}
<h2>Bienvenue !</h2>
<p>Cliquez ici pour confirmer votre adresse e-mail :</p>
<a href="{{ .ConfirmationURL }}">Confirmer l'e-mail</a>
<p>Ou saisissez ce code : <strong>{{ .Token }}</strong></p>
{{ else }}
<h2>Welcome!</h2>
<p>Click here to confirm your email:</p>
<a href="{{ .ConfirmationURL }}">Confirm Email</a>
<p>Or enter this code: <strong>{{ .Token }}</strong></p>
{{ end }}
Example: personalized onboarding based on signup source
If users come from different landing pages or campaigns:
{{ if eq .Data.Domain "https://www.earlyaccess.example.com" }}
<h2>Welcome, Early Access Member!</h2>
<p>You've been granted early access to premium features.</p>
<a href="{{ .ConfirmationURL }}">Activate Early Access</a>
{{ else }}
<h2>Welcome to Example App</h2>
<p>Confirm your email to get started:</p>
<a href="{{ .ConfirmationURL }}">Confirm Email</a>
{{ end }}
Gotchas with Go templates
- Missing fields don’t error - if
{{ .Data.role }}is undefined, it renders as an empty string. Always include an{{ else }}fallback. - Case sensitive -
{{ .Data.Role }}and{{ .Data.role }}are different. - No complex logic - you can’t call external functions, iterate arrays, or do string manipulation beyond basic comparisons.
- Template size limits - the dashboard editor has an undocumented character limit. Keep templates under 64KB.
- No preview - there’s no built-in way to preview rendered templates. Test with real signups in a staging project.
Approach 4: Custom Templates with the Send Email Hook
When dashboard templates aren’t enough - e.g. you need dynamic content from your database, rich HTML rendering frameworks like React Email, or a third-party email provider - the Send Email Hook takes over completely.
The hook intercepts every auth email before it’s sent and delegates delivery to your Edge Function. Supabase’s built-in SMTP is bypassed entirely.
How it works
- A user triggers an auth action (signup, password reset, etc.)
- Instead of sending the email itself, Supabase POSTs the email data to your Edge Function
- Your function receives the full
userobject (with all metadata) andemail_data(token, action type, redirect URL) - Your function decides what to send, how to render it, and which provider delivers it
- Return a
200status to confirm success
This gives you full programmatic control. Any email provider, any templating framework, any business logic. The trade-off is more setup: you need to write, deploy, and maintain an Edge Function.
For a complete walkthrough with code, deployment steps, and edge cases like email change (double confirmation), see our dedicated guide: Supabase Send Email Hook: Customize Auth Emails with Edge Functions.
Common Issues and Fixes
Email prefetching (Safe Links)
Microsoft Defender and some enterprise email filters prefetch links, consuming the confirmation URL before the user clicks it. Fix this by using OTP codes instead of links:
<p>Enter this code to confirm: <strong>{{ .Token }}</strong></p>
Then verify on the client:
const { data, error } = await supabase.auth.verifyOtp({
email,
token: userEnteredCode,
type: 'email'
});
Templates not updating
Template changes in the dashboard can take a few minutes to propagate. If you’re testing locally, make sure your config.toml paths are correct and you’ve restarted supabase start.
User metadata not available in templates
Metadata must be passed at signup time via options.data. You can’t retroactively add metadata and expect it to appear in the signup confirmation template - it’s captured at the moment the email is triggered.
TLDR
For most apps, custom variables and conditionals with user_metadata are enough to handle personalization without deploying an Edge Function. When you outgrow that - need external data lookups, React Email, or provider switching - the Send Email Hook is the escape hatch.
If you’re looking for an email provider that works out of the box with Supabase (both as SMTP and via the SDK in Edge Functions), Pingram offers 3,000 free emails/month and a one-click Supabase integration.