Supabase Auth: Customising Email Templates and Flows Properly
The default Supabase auth emails look like phishing. Here is how to edit the templates, wire up custom SMTP, and align SPF, DKIM, and DMARC so confirmation mail actually lands in Primary.
You shipped the app over the weekend. Signups work, the database is humming, and then a friend texts you a screenshot of your confirmation email sitting in Gmail's spam folder with a grey "via supabase.co" tag next to the sender. The copy is generic, the link looks like a tracker, and the whole thing reads like a phishing attempt. That is the default Supabase auth email experience, and it is the single fastest way to lose users between signup and activation.
The fix is not one thing. It is four things stacked in a specific order. You edit the six built-in templates so the copy and HTML match your brand. You move off the shared Supabase SMTP relay onto a provider you control. You add three DNS records that tell Gmail and Outlook the mail is legitimate. Then you make sure the envelope sender lines up with the visible sender, which is the step almost every tutorial skips and is the reason DMARC keeps failing for people who swear they set everything up correctly.
This guide walks through all four in order, with the HTML you can paste in, the DNS values you need, and the gotchas that only show up once real users start signing up.
Why the default Supabase email is a deliverability trap
When you spin up a Supabase project, auth emails go out through a shared SMTP relay that every other free-tier project on the platform also uses. Three things happen as a result. First, the visible sender is a noreply address at a supabase.co subdomain, which Gmail renders as "via supabase.co" because the domain in the From header does not match the domain that actually signed the message. Second, that shared pool has a mixed reputation because thousands of untested projects send through it, so inbox providers treat it cautiously. Third, there is a hard rate limit of two emails per hour per project, which is fine for local testing and a disaster the moment you get a traffic spike or run a password reset sweep.
The practical result is that the confirmation email either lands in spam, lands late, or does not land at all. Users who do not confirm do not activate, and you never see them again. The default setup is a prototype, not a production system, and Supabase says so explicitly in the production checklist. Moving to custom SMTP is not a nice-to-have, it is the first thing you do before inviting a single real user.
The six templates you actually have to edit
Supabase exposes six email templates under Authentication, Email Templates in the dashboard. Every auth flow your users will ever see maps to one of these, so editing them is not optional if you want the brand to feel coherent.
The Confirm Signup template fires when a new user registers with email and password and needs to click a link to verify ownership. The Magic Link template fires for passwordless signin. The Invite User template fires when you programmatically invite someone through the admin API, typically for team or workspace features. The Change Email Address template fires twice, once to the old address and once to the new, whenever a user updates their email. The Reset Password template fires when someone hits the forgot-password endpoint. The Reauthentication template sends a six-digit nonce for sensitive operations like deleting an account or changing a password while signed in.
Each template accepts a fixed list of Go template variables that Supabase interpolates at send time. The full list is short and worth memorising because the docs bury it in separate pages. You get {{ .ConfirmationURL }} for the full signed verification link, {{ .Token }} for a six-digit one-time code, {{ .TokenHash }} for building your own custom verification link that points at a page you control, {{ .SiteURL }} for the site URL configured in your auth settings, {{ .Email }} for the recipient address, {{ .NewEmail }} and {{ .OldEmail }} for the email change flow, {{ .Data }} for any metadata you stored on the user, and {{ .RedirectTo }} for the post-verification destination if you passed one.
The one you will reach for most often is {{ .Token }}, for a reason most tutorials do not mention. Corporate email scanners like Microsoft Safe Links and various antivirus gateways prefetch every link in inbound mail to check for malware. When they hit the ConfirmationURL first, the token is consumed, and by the time your actual user clicks the link they get "Token has expired or is invalid". Giving users a six-digit code they type into your app instead of a link they click sidesteps the entire class of prefetch problems. Most modern auth flows show both options, a button and a code, and let the user pick.
A brand-voice confirmation template you can paste in
Email HTML is not web HTML. Gmail strips classes, Outlook renders with Word's rendering engine and ignores half of CSS3, and dark mode inverts colours in ways that can turn your black logo into an invisible rectangle on a black background. The template below uses the patterns that actually survive real inboxes: inline styles only, table-based layout for Outlook, a hidden preheader for the inbox preview, system fonts so nothing breaks when web fonts fail to load, and a six-digit code alongside the link.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Confirm your WitsCode account</title>
</head>
<body style="margin:0;padding:0;background:#f5f5f4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#1c1917;">
<span style="display:none !important;opacity:0;color:transparent;height:0;width:0;overflow:hidden;">
Your WitsCode code is {{ .Token }}. Tap the button or paste the code to finish signing up.
</span>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f5f5f4;">
<tr>
<td align="center" style="padding:32px 16px;">
<table role="presentation" width="560" cellpadding="0" cellspacing="0" style="max-width:560px;background:#ffffff;border-radius:12px;border:1px solid #e7e5e4;">
<tr>
<td style="padding:32px 32px 16px 32px;">
<div style="font-size:20px;font-weight:700;letter-spacing:-0.01em;">WitsCode</div>
</td>
</tr>
<tr>
<td style="padding:0 32px;">
<h1 style="font-size:22px;line-height:1.3;margin:8px 0 16px 0;font-weight:600;">Confirm your email to finish signup</h1>
<p style="font-size:15px;line-height:1.6;margin:0 0 24px 0;color:#44403c;">
Thanks for joining WitsCode. Tap the button below to verify {{ .Email }}, or paste the six-digit code into the app if the link does not work in your mail client.
</p>
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="border-radius:8px;background:#1c1917;">
<a href="{{ .ConfirmationURL }}" style="display:inline-block;padding:12px 20px;color:#ffffff;text-decoration:none;font-weight:600;font-size:15px;">Confirm email</a>
</td>
</tr>
</table>
<p style="font-size:13px;line-height:1.6;margin:24px 0 8px 0;color:#78716c;">Or enter this code in the app:</p>
<div style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:22px;font-weight:600;letter-spacing:4px;background:#fafaf9;border:1px solid #e7e5e4;border-radius:8px;padding:12px 16px;display:inline-block;">
{{ .Token }}
</div>
</td>
</tr>
<tr>
<td style="padding:24px 32px 32px 32px;">
<p style="font-size:12px;line-height:1.6;margin:0;color:#a8a29e;">
If you did not create a WitsCode account, ignore this email. The link expires in one hour.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
Paste that into the Confirm Signup template, swap the brand name and colours for your own, and do the same for the other five templates. The only parts that change between templates are the headline copy, the verb on the button, and which variable you use. Reset Password swaps in the reset link, Magic Link drops the password reference, Change Email uses {{ .NewEmail }} in the greeting, and Reauthentication usually shows only the code because it is typed into an already-open session.
Wiring up custom SMTP with Resend, Postmark, or SES
Once the templates look right, the next job is routing the mail through a provider you control. The three common choices are Resend, Postmark, and Amazon SES, and the right one depends on how much you send and how much DNS work you want to do.
Resend is the easiest path for most Supabase projects. The free tier covers three thousand emails a month, the dashboard is clean, and there is a first-party Supabase integration that wires up SMTP automatically when you connect the two accounts. Postmark is the serious-transactional option, with the best deliverability reputation of the three and a strict separation between transactional and broadcast streams, which inbox providers treat as a trust signal. SES is the cheapest at scale but starts in a sandbox that only lets you send to verified addresses until you request production access, and the DNS setup is a bit more ceremony.
Whichever you pick, the Supabase side is identical. In the dashboard go to Project Settings, Authentication, SMTP Settings, and toggle Enable Custom SMTP. Set the sender email to something like auth@send.yourdomain.com, the sender name to your brand, the host to your provider's SMTP host, the port to 587 for STARTTLS or 465 for SSL, and paste the username and password the provider gave you. Save, then trigger a password reset on a test account to make sure mail flows.
The subdomain in that sender address is load-bearing. Using send.yourdomain.com or auth.yourdomain.com instead of yourdomain.com means you verify and build reputation for that subdomain separately. If your marketing team starts sending newsletters from hello@yourdomain.com later and the reputation tanks, your auth mail on the subdomain is insulated. Every mature setup splits transactional and marketing mail across different subdomains or different providers entirely for exactly this reason.
The SPF, DKIM, and DMARC records that actually have to align
This is where most Supabase tutorials wave their hands and move on, and it is the step that determines whether any of the above work matters. Gmail and Yahoo now require authenticated mail with a DMARC policy for any sender that pushes more than a trickle of volume. Getting this right is not optional anymore.
You are adding three TXT or CNAME records to the DNS for the sending subdomain. SPF tells receivers which IP ranges are allowed to send mail claiming to be from your domain. For Resend the record on send.yourdomain.com is v=spf1 include:_spf.resend.com ~all. Postmark uses include:spf.mtasv.net, SES uses include:amazonses.com. DKIM is a public key the provider gives you, usually as a CNAME record at something like resend._domainkey.send.yourdomain.com, which receivers use to verify the cryptographic signature on every outbound message. DMARC goes on the parent domain at _dmarc.yourdomain.com and looks like v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com; adkim=r; aspf=r. Start with p=none so you only get reports, watch the aggregate reports for a week to confirm nothing legitimate is failing, then ratchet up to p=quarantine and eventually p=reject.
The part that trips people up is alignment. DMARC does not just check that SPF and DKIM pass. It checks that the domain that passed SPF or DKIM is the same domain, or a subdomain of, the domain in the From header that the user sees. SPF passes when the Return-Path domain, which is the envelope sender the SMTP server sees, matches the From domain. DKIM passes when the domain in the d= tag of the DKIM signature matches the From domain. You need at least one of those two to align, and relaxed alignment means a subdomain match is fine.
In practice this means the Return-Path that your SMTP provider stamps on outbound mail has to be on your domain, not on the provider's domain. With Resend you verify the sending subdomain and Resend automatically sets the Return-Path to a bounce address on your domain, so alignment just works. With Postmark and SES you have to opt in to custom Return-Path or custom MAIL FROM, which requires an extra MX record on the subdomain and usually an extra TXT record. Skip this step and you will see DKIM pass, SPF pass, and DMARC still fail, because the Return-Path is on amazonses.com or pm-bounces.net and does not align with your From domain. This is the single most common reason people ask "I set up DKIM, why is DMARC still failing".
Verifying end to end before you ship
Before you call it done, send a test email from the Supabase dashboard to a Gmail account and check three things in the Show Original view. The Authentication-Results header should show spf=pass, dkim=pass, and dmarc=pass. The From header should not have a "via" tag when the message renders in the inbox. The Return-Path header should be on your domain or a subdomain of it, not on the provider's domain. If all three check out, do the same on an Outlook.com address and a corporate address if you have access to one, because Microsoft's filters apply different weights than Google's.
The final polish, for brands that care, is BIMI. Once your DMARC is at enforcement with p=quarantine or stricter, you can publish a BIMI record pointing to an SVG of your logo, and Gmail and Apple Mail will render that logo next to every message. It costs nothing if you already have the SVG, and a verified mark certificate from a CA if you want the blue checkmark next to it. Users notice.
What you ship at the end of this
A new signup gets a branded confirmation email from auth@send.yourdomain.com within seconds of hitting the button. It lands in Primary, not Promotions or Spam. It shows your logo, matches the app it came from, and offers both a one-click button and a six-digit code so corporate email scanners do not eat the token. The Authentication-Results headers are clean, the rate limit is whatever your provider allows instead of two per hour, and the DMARC reports in your inbox each morning show everything passing.
If the DNS chase or the Return-Path alignment is the part that stalled you, that is the exact slice WitsCode handles for Supabase projects. We ship the SMTP wiring, the DNS records, the six templates in your brand voice, and a staging inbox test that confirms alignment before anything goes live. Book the Supabase email setup →
Sources:
Get weekly field notes.
Practical writing on shipping products, straight to your inbox. No spam.
Need help with this?
MVP Development
We design and build web apps, MVPs, and SaaS products. Talk to us about what you are working on.
Talk to usWant to discuss vibe coders for your business?
Start a project and we'll talk through where you are, what's working, and the highest-leverage moves for the next 90 days.