Skip to content
Supabase Integration

How to Send Supabase Auth Emails with Sently

Supabase's built-in email sender is meant for testing and only allows a few emails an hour. Send your signup confirmations, magic links and password resets through Sently instead, from your own domain, using Supabase's Send Email Hook.

Written by Ashish Sharda•✓ Based on Supabase's Send Email Hook (Nov 2026)•6 min read

💡 Direct Solution (Executive Summary)

Supabase lets you replace its built-in email sender with a Send Email Hook: a small function that Supabase calls every time it needs to send an auth email. Create a Supabase Edge Function that calls Sently's API, then turn it on under Authentication → Auth Hooks in your Supabase dashboard. Supabase stops using its built-in sender, and every auth email goes out through Sently from your own domain.

Why not just use Supabase's SMTP settings? Sently sends email through its REST API, not SMTP. The Send Email Hook is Supabase's way of plugging in an API-based email service, and it gives you full control over the subject and content of every auth email.

1 Add and Verify Your Domain in Sently

Auth emails must come from a domain you own, such as yourdomain.com or auth.yourdomain.com:

  1. In your Sently dashboard, open Domains and click Add domain.
  2. Add the DNS records Sently shows you at your DNS provider (Cloudflare, Namecheap, GoDaddy and so on). The DMARC record is recommended but optional.
  3. Click Check now. Sently also keeps checking for you, and the domain turns verified once the records are found.

2 Create a Sently API Key

Open API keys and click Create key. Choose live mode and the send access level: the function only needs to send. Copy the key (it starts with sk_live_). It is shown once.

3 Create the Edge Function

With the Supabase CLI, create the function and store your Sently key and sender address as secrets. The sender must be on the domain you verified in step 1:

supabase functions new send-email

supabase secrets set \
  SENTLY_API_KEY="sk_live_..." \
  SENTLY_FROM="Acme <[email protected]>"

Replace the contents of supabase/functions/send-email/index.ts with this. It checks the request really came from Supabase, builds the confirmation link, and sends the email through Sently:

// supabase/functions/send-email/index.ts
import { Webhook } from 'https://esm.sh/[email protected]';

const SENTLY_API_KEY = Deno.env.get('SENTLY_API_KEY')!;
const FROM = Deno.env.get('SENTLY_FROM')!;
const SUPABASE_URL = Deno.env.get('SUPABASE_URL')!;
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',
  invite: 'You have been invited',
  magiclink: 'Your sign-in link',
  email_change: 'Confirm your new email address',
  reauthentication: 'Your verification code',
};

async function send(to: string, type: string, token: string, tokenHash: string, redirectTo: string, key: string) {
  const link = `${SUPABASE_URL}/auth/v1/verify?token=${tokenHash}&type=${type}&redirect_to=${encodeURIComponent(redirectTo)}`;
  const html = type === 'reauthentication'
    ? `<p>Your verification code is <strong>${token}</strong>.</p>`
    : `<p><a href="${link}">Click here to continue</a></p><p>Or enter this code: <strong>${token}</strong></p>`;

  const res = await fetch('https://sently.in/api/v1/emails', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${SENTLY_API_KEY}`,
      'Content-Type': 'application/json',
      // Supabase retries a hook that fails. The same key means a retry
      // never sends the same email twice.
      'Idempotency-Key': key,
    },
    body: JSON.stringify({ from: FROM, to: [to], subject: SUBJECTS[type] ?? 'Your verification email', html }),
  });
  if (!res.ok) throw new Error(`Sently returned ${res.status}: ${await res.text()}`);
}

Deno.serve(async (req) => {
  const payload = await req.text();
  const headers = Object.fromEntries(req.headers);
  try {
    // Proves the request really came from your Supabase project.
    const { user, email_data } = new Webhook(hookSecret).verify(payload, headers) as {
      user: { email: string; new_email?: string };
      email_data: {
        email_action_type: string; token: string; token_hash: string;
        token_new: string; token_hash_new: string; redirect_to: string;
      };
    };
    const { email_action_type: type, token, token_hash, token_new, token_hash_new, redirect_to } = email_data;
    const id = headers['webhook-id'] ?? crypto.randomUUID();

    if (type === 'email_change' && token_hash_new) {
      // Secure email change: one email to each address. Supabase's field
      // names are reversed here on purpose (see its docs).
      await send(user.email, type, token, token_hash_new, redirect_to, `${id}-current`);
      await send(user.new_email!, type, token_new, token_hash, redirect_to, `${id}-new`);
    } else {
      const to = type === 'email_change' ? user.new_email! : user.email;
      await send(to, type, token || token_new, token_hash, redirect_to, id);
    }

    return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
  } catch (error) {
    return new Response(
      JSON.stringify({ error: { http_code: 500, message: String(error) } }),
      { status: 500, headers: { 'Content-Type': 'application/json' } },
    );
  }
});

Deploy it. Supabase signs the request itself, so the function does not need a user's login token:

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

4 Turn On the Send Email Hook in Supabase

  1. In your Supabase dashboard, go to Authentication → Auth Hooks and add a Send Email hook.
  2. Choose HTTPS and enter your function's URL: https://<project-ref>.supabase.co/functions/v1/send-email.
  3. Click Generate secret, copy it, and save the hook.
  4. Store the secret for your function:
supabase secrets set SEND_EMAIL_HOOK_SECRET="v1,whsec_..."

From now on Supabase sends no auth emails itself. Every signup confirmation, magic link, invite, password reset and email change goes through your function and Sently.

5 Test It and Check Your Rate Limits

  • Sign up in your app with a real address, or trigger a password reset. The email should arrive from your own domain within seconds.
  • Open Emails in your Sently dashboard. Every auth email is listed with its status, so you can see when it was delivered or if it bounced.
  • Supabase's own auth rate limits still apply. If you expect many signups at once, raise them under Authentication → Rate Limits.
  • If nothing arrives, check the function's logs in the Supabase dashboard. A message such as Sently returned 403 usually means the sender address is not on a verified domain.

What Sently Adds to Supabase Auth

  • Your own domain: auth emails come from your brand, not a shared address.
  • Delivery events: see when each email was sent, delivered or bounced.
  • Automatic bounce handling: addresses that hard-bounce or mark you as spam are added to your blocked list, so later emails to them are skipped and your domain's reputation is protected.
  • Room to grow: 3,000 free emails a month (up to 150 a day), and no daily limit on paid plans.

Ready to Fix Supabase Email Limits?

Get started with 3,000 free emails per month. No credit card required, and your API key is ready as soon as you sign up.