Recipe: Supabase Auth → Notify (Send Email Hook)

Route Supabase Auth emails (password reset, magic link, confirm signup) through Notify using the Send Email Auth Hook — no SMTP required.

Supabase Auth’s Custom SMTP settings need a real SMTP host. Notify is HTTPS-only (POST /api/email/send), so Custom SMTP cannot point at Notify.

Use Supabase’s Send Email Hook instead: Auth calls your Edge Function; the function verifies the webhook signature and sends with Notify.

That covers password reset, magic link, signup confirm, invite, and email-change messages.

Prerequisites

  • Notify API key
  • Verified domain for your production from address
  • Supabase project with Edge Functions enabled
  • Ability to create an Auth Hook in the dashboard (Authentication → Hooks)

1. Set secrets

supabase secrets set NOTIFY_API_KEY=your_notify_api_key

You will also need SEND_EMAIL_HOOK_SECRET. Generate it when you create the Send Email hook in the Supabase dashboard (step 4). Format looks like v1,whsec_....

supabase secrets set SEND_EMAIL_HOOK_SECRET="v1,whsec_your_secret"

Optional: set a verified sender once:

supabase secrets set NOTIFY_FROM="Auth <noreply@your-verified-domain.com>"

2. Edge Function

supabase functions new send-email

supabase/functions/send-email/index.ts:

import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0';

const hookSecret = (Deno.env.get('SEND_EMAIL_HOOK_SECRET') ?? '').replace(
  'v1,whsec_',
  ''
);
const notifyKey = Deno.env.get('NOTIFY_API_KEY')!;
const from =
  Deno.env.get('NOTIFY_FROM') ?? 'noreply@your-verified-domain.com';

const subjects: Record<string, string> = {
  signup: 'Confirm your email',
  recovery: 'Reset your password',
  invite: "You've been invited",
  magiclink: 'Your magic link',
  email_change: 'Confirm your email change',
  email_change_new: 'Confirm your email change',
  reauthentication: 'Your verification code'
};

Deno.serve(async (req) => {
  if (req.method !== 'POST') {
    return new Response('not allowed', { status: 400 });
  }

  const payload = await req.text();
  const headers = Object.fromEntries(req.headers);
  const wh = new Webhook(hookSecret);

  try {
    const { user, email_data } = wh.verify(payload, headers) as {
      user: { email: string };
      email_data: {
        token: string;
        token_hash: string;
        redirect_to: string;
        email_action_type: string;
        site_url: string;
      };
    };

    const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
    const verifyUrl =
      `${supabaseUrl}/auth/v1/verify` +
      `?token=${email_data.token_hash}` +
      `&type=${email_data.email_action_type}` +
      `&redirect_to=${encodeURIComponent(email_data.redirect_to)}`;

    const action = email_data.email_action_type;
    const subject = subjects[action] ?? 'Your authentication email';
    const html = `
      <p>Use this link to continue:</p>
      <p><a href="${verifyUrl}">${verifyUrl}</a></p>
      <p>Or enter this code: <strong>${email_data.token}</strong></p>
    `;

    const res = await fetch('https://notify.cx/api/email/send', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': notifyKey
      },
      body: JSON.stringify({
        to: user.email,
        from,
        subject,
        html
      })
    });

    if (!res.ok) {
      const body = await res.text();
      throw new Error(`Notify send failed: ${res.status} ${body}`);
    }
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    return new Response(
      JSON.stringify({
        error: {
          http_code: 500,
          message
        }
      }),
      {
        status: 401,
        headers: { 'Content-Type': 'application/json' }
      }
    );
  }

  return new Response(JSON.stringify({}), {
    status: 200,
    headers: { 'Content-Type': 'application/json' }
  });
});

Notes:

  • Prefer token_hash in the verify URL (not the raw OTP alone as the only path).
  • Lead with html for the body (message / text also work).
  • A non-2xx response from the hook fails the Auth email step — keep the function fast and reliable.

Deploy with JWT verification off so Auth can call the function:

supabase functions deploy send-email --no-verify-jwt

3. Point Auth at the function

In the Supabase dashboard: Authentication → Hooks → Send Email.

  1. Choose HTTPS
  2. URL: https://<project-ref>.supabase.co/functions/v1/send-email
  3. Generate / paste the hook secret (same value as SEND_EMAIL_HOOK_SECRET)
  4. Save

While the hook is enabled, it replaces built-in Auth email delivery for that project.


4. Smoke test

  1. Trigger a password reset or signup confirm for an address you control
  2. Confirm the message in Notify logs
  3. Open the link — it should hit /auth/v1/verify and redirect

If nothing arrives: check Edge Function logs, hook secret mismatch, and that from uses a verified Notify domain (and that the account is not stuck in sandbox-only).


Why not Custom SMTP?

PathWorks with Notify?
Supabase Custom SMTP (host / port / user / password)No — Notify has no SMTP relay
Supabase Send Email Hook → Edge Function → Notify HTTPYes

For a generic Edge Function you invoke yourself (not Auth’s hook), see Send email from Supabase Edge Functions.

Where to next?