Supabase Auth Email
Purpose
Configure Supabase transactional auth email end-to-end — custom SMTP (e.g. Resend), branded templates applied via the Management API, deliverability, and reliable sign-up confirmation and password-reset flows — avoiding the silent gates, 500s, and consumed links that block auth emails in production.
When to use
When setting up or debugging Supabase auth emails: sign-up confirmation, password reset, or magic links. Apply when configuring custom SMTP, applying branded templates, chasing an HTTP 500 on sign-up, or when confirm links "work" but accounts never confirm. Especially relevant on newer Supabase projects, which lock template editing until custom SMTP is set.
Inputs expected
- Supabase project ref and a Management API personal access token (the CLI login token cannot be reused for the API — see principles)
- Email provider account and API key (Resend assumed; any SMTP works)
- The sending domain and access to its DNS host
- App name and the confirm/reset redirect URLs
Guiding principles
- Custom auth-email template editing is gated by project creation date, not plan. A restriction added ~mid-2026 locks template editing (via both the Management API and the dashboard) on newly created free projects until custom SMTP is configured or the org is on Pro. Projects created before it keep free dashboard editing. If template edits silently do nothing, this is why.
- Configure custom SMTP first — it unlocks templates and improves deliverability. For Resend: host
smtp.resend.com, port465, username the literal lowercaseresend, password a Resend API key with Sending scope. A capitalisedResendusername fails auth with "535 Invalid username" — which surfaces only at send time as an HTTP 500 on sign-up. See Resend SMTP inreference.md. - PATCH the full SMTP group together — a single
smtp_*field clears the rest. On the Management API (/v1/projects/{ref}/config/auth), PATCHing onesmtp_*field empties the whole SMTP group (host/user/pass go blank). Send the complete SMTP config in one PATCH. Flat fields likemailer_*anduri_allow_listpatch independently and safely. - Apply branded templates via the API last, and re-apply after any dashboard change. Saving the dashboard Emails/SMTP page overwrites Management-API-pushed templates with the dashboard's stale defaults. Apply templates via the API after SMTP is set, and re-run the apply after any dashboard email/SMTP edit.
- Templates have no runtime app-name variable — bake it at apply time. Templates expose only Go tokens (
{{ .ConfirmationURL }},{{ .SiteURL }},{{ .Email }}). Substitute the app name via__APP_NAME__when applying; on a rebrand, re-run the apply. The "From" display name is a separatesmtp_sender_namefield in the SMTP group that the template apply does not touch — change it in the dashboard, then re-run the template apply. - With
mailer_autoconfirm = false, email confirmation is a hard sign-in gate. An unconfirmed account exists but cannot log in, so the app must surface a "confirm then sign in" state plus a resend action. A sign-up on an already-registered email is detected client-side by an emptyidentitiesarray on the returned user (anti-enumeration otherwise makes it look like success) — surface that rather than silently "succeeding". - Host reset-password / confirm-email as static pages driven by one same-origin config script. Read every environment value from a tiny
auth-config.js(window.<APP>_AUTH) so a dev-to-prod Supabase project switch is a one-file change; widen those pages' CSPconnect-srctohttps://*.supabase.coso the project swap needs no CSP edits. Only the anon/publishable key ever goes in the file. - Harden the hosted auth pages. Load supabase-js from a version-pinned CDN URL with SRI and guard for the library being absent so the page fails closed to an error state; create the client with
persistSession: falseso no session is written to the visitor's browser; implement noredirect_tohandling (open-redirect vector) — navigation targets stay hardcoded to the portal origin and the app scheme. - Exclude the hosted auth page paths from mobile deep-link scopes (
/reset-password,/confirm-emailout of AASAcomponentsand AndroidintentFilters); if the app captures those URLs, the browser-based reset/confirm flows break mid-flight. - A password-reset
redirectTomust be in the redirect allow-list. Add it under Auth → URL Configuration → Redirect URLs (oruri_allow_listvia the API). Otherwise Supabase ignores it and falls back to Site URL, so the reset link never reaches your page. - Don't use the default
{{ .ConfirmationURL }}server-side confirm link. Mail-security scanners (Outlook Safe Links, etc.) pre-fetch and consume it, so accounts never confirm. Instead link to a hosted page that completes confirmation client-side viaverifyOtp({ token_hash, type: 'signup' }), then deep-links back into the app. See Client-side confirm page inreference.md. - A failed email send returns HTTP 500 on
/auth/v1/signupbut still creates the user row. The real SMTP error (e.g. "535 Invalid username") is in the project'sauth_logs, queryable via the Management API analytics logs endpoint — check there, not the HTTP response. - Verify the sending domain in DNS, and expect a warm-up period. Resend needs DKIM on
resend._domainkey, MX + SPF on asendsubdomain, and DMARC on_dmarc, added at the domain's actual DNS host (which may differ from the registrar or web host). These sit on different hostnames than existing Microsoft 365 mail records, so they don't conflict. A brand-new sending domain has no reputation, so early emails often land in spam even when SPF/DKIM pass — expected, and it improves as the domain sends legitimate mail. - The Supabase CLI login token is not a Management API token. It lives in Windows Credential Manager (LegacyGeneric target "Supabase CLI:supabase") and cannot be reused for the Management API, which needs a personal access token (optionally at
%USERPROFILE%\.supabase\access-token); ask the project owner for a token rather than trying to extract vault credentials.supabase projects listprints "Cannot find project ref. Have you run supabase link?" to stderr yet still lists every project, and truncating its output (Select-Object -First N) hid the production row and produced a wrong "different org" conclusion — filter noise instead of truncating. - From PowerShell 5.1, send the Management API body as UTF-8 bytes. The default string encoding is Latin-1, which mangles UTF-8 and returns 400 errors. Encode the JSON with
[System.Text.Encoding]::UTF8.GetBytes($json)and read template files with[System.IO.File]::ReadAllText($path,[Text.Encoding]::UTF8). See reference.md.
Process
- Set up custom SMTP — create the provider account and API key, verify the sending domain in DNS, then PATCH the full SMTP group to the Management API. See
reference.md. - Confirm template editing is unlocked — on a newer project, editing stays locked until SMTP is set.
- Apply branded templates via the API, substituting
__APP_NAME__, after SMTP is configured. Setsmtp_sender_namein the dashboard, then re-apply. - Add redirect/confirm URLs to the allow-list (
uri_allow_list). - Switch confirmation to a client-side
verifyOtppage so Safe Links cannot consume the link — hosted statically, config fromauth-config.js, pinned+SRI supabase-js,persistSession: false, noredirect_to, and excluded from app deep-link scopes. - Handle the unconfirmed state in the app — a "confirm then sign in" screen with resend, and treat an empty
identitiesarray on sign-up as "already registered". - Test sign-up and reset; on a 500, read
auth_logsfor the real SMTP error.
Output format
- SMTP configuration — provider, the full PATCHed SMTP group, sender name
- DNS records — DKIM/SPF/MX/DMARC entries added, verification status
- Templates applied — which templates, app name baked in, applied after SMTP
- Flow config — allow-listed redirect URLs, client-side confirm page in place, unconfirmed/already-registered states handled in the app
- Verification — sign-up and reset tested;
auth_logsclean
Quality checklist
- Custom SMTP set via a single full-group PATCH (no partial
smtp_*PATCH) - Resend username is lowercase
resend; password is a Sending-scope API key - Branded templates applied via the API after SMTP; re-applied after any dashboard save
-
smtp_sender_nameset (separately) and current after any rename - Confirm flow uses a client-side
verifyOtppage, not the default{{ .ConfirmationURL }} - Hosted auth pages: one
auth-config.js(anon key only), CSPconnect-src https://*.supabase.co, pinned CDN + SRI with fail-closed guard,persistSession: false, noredirect_to, paths excluded from AASA/intent-filter scopes - Management API calls use a personal access token, not the CLI login token;
supabase projects listoutput filtered, not truncated - Reset/confirm redirect URLs added to
uri_allow_list - App shows "confirm then sign in" + resend for unconfirmed accounts; empty
identitieson sign-up surfaced as already-registered - Sending domain DKIM/SPF/MX/DMARC verified; spam warm-up expected
- Sign-up/reset tested;
auth_logschecked on any HTTP 500
Avoid
- Editing templates on a newer project before setting custom SMTP — editing is silently locked
- PATCHing a single
smtp_*field — it clears the whole SMTP group; send the full config together - Applying templates before SMTP, or forgetting to re-apply after a dashboard save — the dashboard reverts them
- Assuming
smtp_sender_nameupdates with the template apply — it is separate and stays stale after a rename - Using the default
{{ .ConfirmationURL }}— Safe Links pre-fetch and consume it; use a client-sideverifyOtppage - Trusting the HTTP response on send failure — the 500 hides the real error; read
auth_logs - Hardcoding the Supabase URL/key into each hosted auth page, or honouring a
redirect_toparameter on them — one config script, fixed navigation targets - Letting the app's App Links / Universal Links capture
/reset-passwordor/confirm-email— the browser flow breaks mid-flight - Reusing the CLI login token for the Management API, or
Select-Object -First Nonsupabase projects list— use a PAT and filter, don't truncate - Sending the Management API body as a default PowerShell string — encode as UTF-8 bytes or get 400s
- Treating early spam-foldering on a new domain as a misconfiguration — it is reputation warm-up
- Treating a sign-up response with an empty
identitiesarray as a fresh account — the email is already registered; anti-enumeration hides the error
Example usage
"New Supabase project — sign-up returns a 500 and no confirmation email arrives, and the dashboard won't let me edit the email templates. Set up Resend SMTP, apply branded templates via the Management API, fix the confirm flow so Outlook doesn't eat the link, and get password reset redirecting to my page."
Source: This skill is sourced from the Matrix Skills library. Learn more at the AI Agent Skills Library.