Supabase

An Edge Function for your own mail, and a custom auth hook for Supabase's.

Two different jobs live here. Sending your own transactional mail is an Edge Function. Replacing the confirmation and reset emails Supabase Auth sends is the Send Email Hook, and it is the reason most people arrive at this page: the built-in SMTP is rate limited and not meant for production.

Set the key

Secrets for Edge Functions
# The key comes from https://emails.sh/dashboard/api-keys.
npx supabase secrets set EMAILSSH_API_KEY=esh_your_key_here

# For local development, add it to supabase/.env (gitignored):
echo 'EMAILSSH_API_KEY=esh_your_key_here' >> supabase/.env

An Edge Function

supabase/functions/send-email/index.ts
// supabase/functions/send-email/index.ts
Deno.serve(async (request) => {
  const { to, subject, html } = await request.json();

  const response = await fetch('https://emails.sh/v1/emails', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${Deno.env.get('EMAILSSH_API_KEY')}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ from: 'Acme <hello@acme.com>', to: [to], subject, html })
  });

  const body = await response.json();
  if (!response.ok) {
    console.error('emails.sh refused the send', body.error.code, body.error.next ?? body.error.message);
    return new Response(JSON.stringify({ error: 'Could not send that email.' }), { status: 502 });
  }

  return new Response(JSON.stringify({ id: body.id }), {
    headers: { 'content-type': 'application/json' }
  });
});
Deploy it
npx supabase functions deploy send-email

Auth emails through the Send Email Hook

Supabase calls a function of yours instead of sending the mail itself, passing the user and the token. You render the email and send it, which means the confirmation email finally looks like your product.

supabase/functions/auth-email/index.ts
// supabase/functions/auth-email/index.ts
// Set the hook to this function's URL under Authentication, Hooks, Send Email.
Deno.serve(async (request) => {
  const { user, email_data } = await request.json();
  const link = `${email_data.site_url}/auth/confirm?token_hash=${email_data.token_hash}&type=${email_data.email_action_type}`;

  const subjects: Record<string, string> = {
    signup: 'Confirm your Acme account',
    recovery: 'Reset your Acme password',
    magiclink: 'Your Acme sign-in link',
    email_change: 'Confirm your new email address'
  };

  const response = await fetch('https://emails.sh/v1/emails', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${Deno.env.get('EMAILSSH_API_KEY')}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      from: 'Acme <hello@acme.com>',
      to: [user.email],
      subject: subjects[email_data.email_action_type] ?? 'Acme',
      html: `<p>Click to continue: <a href="${link}">${link}</a></p><p>The link expires in an hour.</p>`,
      text: `Click to continue: ${link}\n\nThe link expires in an hour.`,
      tags: { template: email_data.email_action_type }
    })
  });

  if (!response.ok) {
    const refusal = await response.json();
    // Returning an error tells Supabase the mail did not go, so the user is
    // told rather than left waiting for something that never arrives.
    return new Response(JSON.stringify({ error: { message: refusal.error.message } }), { status: 500 });
  }

  return new Response('{}', { headers: { 'content-type': 'application/json' } });
});