Code Tips12 min read

Supabase Auth Emails with React Email and Resend: Replace Default Templates with Custom Designs

Use Supabase's Send Email auth hook to replace default auth emails with branded React Email templates sent through Resend. Full Edge Function setup guide.

R

React Emails Pro

March 16, 2026

Supabase ships with built-in auth emails for confirmation, password reset, and magic links. They work. They also look like they were designed in 2014 by someone who had never seen your product.

The default templates are plain HTML with minimal styling, no brand colors, no logo, and no way to customize them beyond swapping a few variables in the dashboard. For a side project, that's fine. For anything with paying users, it's a problem — your auth emails are often the first thing a new user sees after signing up.

Supabase added the Send Email auth hook specifically to solve this. It lets you intercept every auth email, render it with whatever templating system you want, and send it through your own provider. This guide walks through the full setup with React Email and Resend.

35%

Users judge a brand by its emails

Litmus, 2025 State of Email

4.2x

Higher click rates on branded emails

Compared to plain-text defaults

6

Auth email types Supabase supports

Confirmation, reset, magic link, invite, change, reauthentication


How the Supabase Send Email hook works

The architecture is straightforward. Instead of Supabase sending auth emails directly through its built-in SMTP, you register an HTTPS endpoint as a “Send Email” hook. When any auth event triggers an email, Supabase sends a POST request to your endpoint with the email type, recipient, and token data. Your endpoint renders the template and sends it through your ESP.

  1. User triggers an auth action (sign up, reset password, magic link)
  2. Supabase calls your hook endpoint with email_data containing the token, redirect URL, and email type
  3. Your Edge Function picks the right React Email template, renders it to HTML
  4. Resend (or any ESP) delivers the email
  5. Your function returns a JSON response so Supabase knows the email was sent
The hook replaces all auth emails. Once enabled, Supabase stops sending its own templates entirely. Make sure you handle every email type before enabling it in production.

Setting up the Edge Function

The Send Email hook runs as a Supabase Edge Function. Create a new function with supabase functions new send-email and wire it up in your project dashboard under Authentication → Hooks.

Here's the full handler. It receives the webhook payload, maps the email type to a React Email component, renders it, and sends via Resend.

supabase/functions/send-email/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { Resend } from "npm:resend@4.0.0";
import { render } from "npm:@react-email/render@1.0.0";
import { ConfirmationEmail } from "./templates/confirmation.tsx";
import { PasswordResetEmail } from "./templates/password-reset.tsx";
import { MagicLinkEmail } from "./templates/magic-link.tsx";

const resend = new Resend(Deno.env.get("RESEND_API_KEY")!);

interface WebhookPayload {
  user: {
    email: string;
    user_metadata?: { full_name?: string };
  };
  email_data: {
    token: string;
    token_hash: string;
    redirect_to: string;
    email_action_type: string;
    site_url: string;
    token_new?: string;
    token_hash_new?: string;
  };
}

const TEMPLATES: Record<string, (props: TemplateProps) => JSX.Element> = {
  signup: ConfirmationEmail,
  recovery: PasswordResetEmail,
  magic_link: MagicLinkEmail,
  email_change: ConfirmationEmail,
  invite: ConfirmationEmail,
};

interface TemplateProps {
  name: string;
  token: string;
  redirectTo: string;
  siteUrl: string;
}

serve(async (req: Request) => {
  const payload: WebhookPayload = await req.json();
  const { user, email_data } = payload;
  const emailType = email_data.email_action_type;

  const Template = TEMPLATES[emailType];
  if (!Template) {
    return new Response(
      JSON.stringify({ error: `Unknown email type: ${emailType}` }),
      { status: 400, headers: { "Content-Type": "application/json" } }
    );
  }

  const confirmationUrl = new URL(email_data.redirect_to);
  confirmationUrl.searchParams.set("token_hash", email_data.token_hash);
  confirmationUrl.searchParams.set("type", emailType);

  const html = await render(
    Template({
      name: user.user_metadata?.full_name ?? user.email.split("@")[0],
      token: email_data.token,
      redirectTo: confirmationUrl.toString(),
      siteUrl: email_data.site_url,
    })
  );

  const SUBJECTS: Record<string, string> = {
    signup: "Confirm your email",
    recovery: "Reset your password",
    magic_link: "Your sign-in link",
    email_change: "Confirm your new email",
    invite: "You've been invited",
  };

  const { error } = await resend.emails.send({
    from: "YourApp <auth@yourdomain.com>",
    to: user.email,
    subject: SUBJECTS[emailType] ?? "Action required",
    html,
  });

  if (error) {
    return new Response(JSON.stringify({ error }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });
  }

  return new Response(JSON.stringify({}), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  });
});
The hook must return a 200 status with an empty JSON object on success. Returning anything else causes Supabase to fall back to its default emails — or worse, fail silently.

Building the React Email templates

Each auth email type gets its own React Email component. The key is keeping them simple — auth emails should be functional, branded, and fast to scan. Nobody reads a 500-word password reset email.

Here's a confirmation email template. It includes your logo, a clear heading, one CTA button, and a fallback link for email clients that strip buttons.

supabase/functions/send-email/templates/confirmation.tsx
import {
  Html,
  Head,
  Body,
  Container,
  Section,
  Text,
  Button,
  Hr,
  Preview,
} from "@react-email/components";

interface ConfirmationEmailProps {
  name: string;
  token: string;
  redirectTo: string;
  siteUrl: string;
}

export function ConfirmationEmail({
  name,
  redirectTo,
  siteUrl,
}: ConfirmationEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Confirm your email to get started</Preview>
      <Body style={body}>
        <Container style={container}>
          <Text style={heading}>Confirm your email</Text>
          <Text style={paragraph}>
            Hey {name}, thanks for signing up. Click the button below to
            confirm your email address and activate your account.
          </Text>
          <Section style={buttonSection}>
            <Button style={button} href={redirectTo}>
              Confirm email address
            </Button>
          </Section>
          <Hr style={hr} />
          <Text style={footer}>
            If the button doesn&apos;t work, copy and paste this link into your
            browser: {redirectTo}
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

const body = {
  backgroundColor: "#f9fafb",
  fontFamily:
    '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};

const container = {
  maxWidth: "480px",
  margin: "40px auto",
  padding: "32px",
  backgroundColor: "#ffffff",
  borderRadius: "8px",
};

const heading = {
  fontSize: "24px",
  fontWeight: "600" as const,
  color: "#111827",
  marginBottom: "16px",
};

const paragraph = {
  fontSize: "15px",
  lineHeight: "1.6",
  color: "#374151",
};

const buttonSection = {
  textAlign: "center" as const,
  margin: "24px 0",
};

const button = {
  backgroundColor: "#111827",
  color: "#ffffff",
  fontSize: "15px",
  fontWeight: "500" as const,
  padding: "12px 32px",
  borderRadius: "6px",
  textDecoration: "none",
};

const hr = {
  borderColor: "#e5e7eb",
  margin: "24px 0",
};

const footer = {
  fontSize: "12px",
  lineHeight: "1.5",
  color: "#9ca3af",
  wordBreak: "break-all" as const,
};

The password reset and magic link templates follow the same pattern — swap the heading, body copy, and CTA text. The important thing is consistency: same layout, same brand, same voice across all auth emails.

Include a Preview component in every template. It controls the preview text shown in inbox list views — the text that often determines whether a user opens the email or ignores it.

Default Supabase emails vs custom React Email

Custom React Email templates
  • Full brand control — logo, colors, typography
  • Responsive layout tested across clients
  • Custom subject lines per email type
  • Send through your own ESP (better deliverability)
  • Version control and code review for templates
  • A/B test subject lines and layouts
Default Supabase emails
  • Generic HTML with no branding
  • Limited to variable swaps in the dashboard
  • Same subject line format for all emails
  • Sent through Supabase's shared SMTP
  • No version history or review process
  • No testing infrastructure

Deploying to production with Resend

Resend is the simplest ESP to pair with React Email since they're built by the same team. But the hook works with any provider that accepts HTML — SendGrid, Postmark, Amazon SES.

Production checklist

  1. Verify your sending domain in Resend. Add the DNS records (SPF, DKIM, DMARC) that Resend generates. Without this, your emails land in spam.
  2. Set environment variables. Add RESEND_API_KEY to your Edge Function secrets via supabase secrets set RESEND_API_KEY=re_xxxxx.
  3. Deploy the function. Run supabase functions deploy send-email. Note the function URL.
  4. Enable the hook. In your Supabase dashboard, go to Authentication → Hooks, add a new “Send Email” hook with the HTTPS type, and paste your function URL.
  5. Test every email type. Trigger a real sign-up, a password reset, and a magic link. Check rendering in Gmail and Outlook at minimum.
Don't skip domain verification. Supabase's default SMTP uses their domain, but once you switch to Resend, you're sending from your domain. Unverified domains get flagged immediately.

Testing and debugging the hook

Auth email hooks are notoriously hard to debug because the feedback loop is slow: trigger an auth event, wait for the email, check your inbox, repeat. Here are patterns that speed this up.

Local development

  • Use supabase functions serve send-email to run the function locally.
  • Send test payloads with curl to verify rendering without triggering real auth events.
  • Use React Email's built-in preview server (email dev) to iterate on template design before wiring up the hook.

Common failure modes

  • Hook returns non-200: Supabase falls back to default emails. Check your function logs with supabase functions logs send-email.
  • Email sent but not received: Check Resend's dashboard for delivery status. Usually a domain verification issue.
  • Token expired by the time user clicks: Your redirect URL construction might be wrong. Double-check that token_hash and type are both in the query params.
  • Template renders but looks broken: Test the HTML output in Litmus or Email on Acid. Outlook is usually the culprit.
Test the hook locally
# Send a test payload to your local function
curl -X POST http://localhost:54321/functions/v1/send-email \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_ANON_KEY" \
  -d '{
    "user": {
      "email": "test@example.com",
      "user_metadata": { "full_name": "Test User" }
    },
    "email_data": {
      "token": "test-token-123",
      "token_hash": "abc123",
      "redirect_to": "http://localhost:3000/auth/confirm",
      "email_action_type": "signup",
      "site_url": "http://localhost:3000"
    }
  }'

Handling all six email types

Supabase can trigger six different auth email types. If you enable the hook and miss one, that email silently fails. Here's the complete list with the email_action_type values:

  • signup — Confirm your email after registration
  • recovery — Password reset request
  • magic_link — Passwordless sign-in link
  • invite — Team/org invitation email
  • email_change — Confirm new email address
  • reauthentication — Re-verify identity for sensitive actions
You don't need six unique designs. Most projects use 2–3 visual layouts: one for confirmations (signup, invite, email change), one for credential actions (recovery, reauthentication), and one for magic links. Reuse the layout, change the copy.

Key takeaway

Supabase + React Email + Resend checklist:

  • Create an Edge Function that handles the Send Email hook payload
  • Build React Email templates for all 6 auth email types
  • Verify your sending domain in Resend (SPF, DKIM, DMARC)
  • Deploy the function and enable the hook in the Supabase dashboard
  • Test every email type with real auth events before going live
  • Monitor delivery via Resend's dashboard and function logs
R

React Emails Pro

Team

Building production-ready email templates with React Email. Writing about transactional email best practices, deliverability, and developer tooling.

Production-ready templates

Pick from 9 template packs built with React Email. One-time purchase, lifetime updates, tested across every major email client.

Browse all templates