How to send Supabase auth emails with your own email API
Short answer: Turn on Supabase's Send Email Hook and point it at an Edge Function. Supabase sends the function the user and a one-time token, the function builds the link and sends the email through SendFox's API. SendFox doesn't offer SMTP, so the hook is the way in. Free for 3,000 emails a month.
Supabase's built-in email sender is for testing: it's rate limited and sends from Supabase's domain. For production you want auth emails from your own domain with delivery logs, and that's what this sets up.
Step 1: Get an API key
Sign up free and open Transactional. Your key is created on the first visit.
Step 2: Write the Edge Function
It checks the hook's signature, builds the confirmation link from token_hash, and sends one email. The six-digit token goes in the email too, for apps that sign in with a code.
supabase/functions/send-email/index.ts
import { Webhook } from 'https://esm.sh/[email protected]';
const hookSecret = Deno.env.get('SEND_EMAIL_HOOK_SECRET')!.replace('v1,whsec_', '');
const subjects: Record<string, string> = {
signup: 'Confirm your email',
recovery: 'Reset your password',
magiclink: 'Your sign-in link',
invite: 'You have been invited',
email_change: 'Confirm your new email',
reauthentication: 'Your verification code',
};
// Click tracking can never be turned on for these tags, so scanners can't burn the link.
const tags: Record<string, string> = {
signup: 'verify_email',
recovery: 'password_reset',
magiclink: 'magic_link',
email_change: 'verify_email',
reauthentication: 'otp',
};
Deno.serve(async (req) => {
const payload = await req.text();
const { user, email_data } = new Webhook(hookSecret).verify(
payload,
Object.fromEntries(req.headers),
) as { user: { email: string; new_email?: string }; email_data: Record<string, string> };
const type = email_data.email_action_type;
// A secure email change sends two emails. Supabase's naming is inverted here:
// token_hash_new goes to the current address, token_hash to the new one.
const emails = type === 'email_change' && user.new_email
? [
{ to: user.email, tokenHash: email_data.token_hash_new, token: email_data.token_new },
{ to: user.new_email, tokenHash: email_data.token_hash, token: email_data.token },
]
: [{ to: user.email, tokenHash: email_data.token_hash, token: email_data.token }];
for (const email of emails.filter((e) => e.tokenHash)) {
const link = `${Deno.env.get('SUPABASE_URL')}/auth/v1/verify?token=${email.tokenHash}`
+ `&type=${type}&redirect_to=${encodeURIComponent(email_data.redirect_to)}`;
const res = await fetch('https://api.sendfox.com/transactional/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${Deno.env.get('SENDFOX_API_KEY')}`,
'Content-Type': 'application/json',
'Idempotency-Key': `supabase-${email.tokenHash}`,
},
body: JSON.stringify({
from: 'Acme <[email protected]>',
to: [email.to],
subject: subjects[type] ?? 'Your Acme account',
html: `<p><a href="${link}">Continue</a></p><p>Or enter this code: <strong>${email.token}</strong></p>`,
tag: tags[type] ?? 'auth',
}),
});
if (!res.ok) {
const error = await res.json();
return Response.json({ error: { http_code: res.status, message: `${error.name}: ${error.fix}` } }, { status: 500 });
}
}
return Response.json({});
});
Supabase changes hook details now and then, especially for email changes. If anything here disagrees with Supabase's Send Email Hook docs, follow theirs for the hook side; the SendFox call stays the same.
Step 3: Deploy it and set secrets
The hook authenticates with its own signature, not a user JWT, so deploy without JWT verification:
supabase functions deploy send-email --no-verify-jwt
supabase secrets set SENDFOX_API_KEY=your_sendfox_key
supabase secrets set SEND_EMAIL_HOOK_SECRET='v1,whsec_...'
Step 4: Turn on the hook
In the Supabase dashboard, open Authentication > Hooks, add a Send Email hook of type HTTPS, and paste your function's URL. Supabase generates the hook secret there; that's the value for SEND_EMAIL_HOOK_SECRET.
Step 5: Test, then verify your domain
Send to [email protected] first. Test addresses never reach a mailbox and never count against your quota, and [email protected] and [email protected] fake those outcomes so you can see your error handling and webhook fire. Until your domain is verified, send from [email protected], which delivers to the test addresses and your own account email.
Then add your domain under Account > Domains, paste the DNS records into your DNS provider and click verify. After that, send from any address on it.
Things to know
- Errors reach Supabase. If the send fails, the function returns the API's error and fix, and Supabase shows it to the sign-up call, so you see
domain_not_verifiedinstead of a silent missing email. - Retries are safe. The idempotency key is built from the one-time token hash, so if Supabase retries the hook, SendFox returns the original email instead of sending another.
- Every auth email in one place. The live feed on the Transactional page shows each one with its delivery events, which is the first place to look when a user says "I never got the email".
Full reference: Transactional API docs, or the same docs as Markdown to hand to your coding agent.
FAQ
Why use the Send Email Hook instead of custom SMTP?
Supabase can send auth emails through custom SMTP or through the Send Email Hook. SendFox does not offer SMTP, so use the hook: Supabase calls your function with the user and the token, and your function sends the email over HTTPS.
Do I still hit Supabase's built-in email rate limit?
The built-in sender is meant for testing and is tightly limited. With the hook, your own provider sends the email, so the limits are your SendFox plan limits instead.
Which emails does the hook cover?
Every auth email: signup confirmation, magic link, password recovery, invite, email change and reauthentication. The email_action_type field says which one.
How much does it cost?
Free for 3,000 emails a month. $15 a month for 50,000.
Send your first email in two minutes.
Free for 3,000 emails a month. $15 a month for 50,000. Already on Resend? Change two environment variables.
Get your free API key