Send Supabase Auth email with Samva

Send Supabase Auth transactional email through Samva with a signed Send Email Hook.

Use a Supabase Auth Send Email Hook to route signup, invite, magic link, recovery, email change, and reauthentication emails through Samva. Supabase owns auth; your hook verifies the signed request, renders the email, and calls samva.messages.send.

Samva sends from the verified sender configured on your account. The hook does not pass a from value.

Before you start

You need:

  • A Supabase project with Auth enabled and an application URL configured for its redirect allowlist.
  • A Samva workspace with a verified sending domain and a server-side API key.
  • A Supabase Edge Function or another HTTPS endpoint that Supabase can call. Keep SAMVA_API_KEY and SEND_EMAIL_HOOK_SECRET in that server environment; neither belongs in browser code.

The hook replaces Supabase's built-in email and Custom SMTP delivery for the covered Auth flows. It does not replace Supabase Auth, store users, or manage redirect allowlists.

Install

Create an Edge Function for the hook, then add the SDK, signature verifier, and email renderer to its Deno import map. The complete example pins these imports in deno.json:

supabase functions new send-email

Add these imports to your project's deno.json (the example keeps it at the repository root):

deno.json
{
  "imports": {
    "react": "npm:react@19.2.7",
    "react/jsx-runtime": "npm:react@19.2.7/jsx-runtime",
    "react-email": "npm:react-email@6.6.5",
    "samva": "npm:samva@0.3.0",
    "standardwebhooks": "npm:standardwebhooks@1.0.0"
  }
}

For a non-Edge-Function endpoint, install samva, standardwebhooks, and your chosen HTML email renderer with that runtime's package manager instead.

Hook config

Enable the Send Email Hook in supabase/config.toml:

[auth.hook.send_email]
enabled = true
uri = "https://<your-endpoint>"
secrets = "env(SEND_EMAIL_HOOK_SECRET)"

For local Supabase CLI development, point the URI at your served Edge Function:

uri = "http://host.docker.internal:54321/functions/v1/send-email"

Set server-only secrets in the function environment:

SAMVA_API_KEY=samva_sk_live_...
SEND_EMAIL_HOOK_SECRET=v1,whsec_<base64-secret>
SUPABASE_PROJECT_REF=your-project-ref

Serve Supabase Edge Functions with JWT verification disabled, because the Auth hook fires before a user JWT exists:

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

Enabling the hook overrides Supabase's built-in email and Custom SMTP sending for the covered Auth flows.

Endpoint shape

The handler reads the raw body, verifies the Standard Webhooks signature, renders by email_action_type, and returns an empty 200 on success.

import { Webhook } from "standardwebhooks";
import { createClient } from "samva";

const apiKey = Deno.env.get("SAMVA_API_KEY");
const configuredSecret = Deno.env.get("SEND_EMAIL_HOOK_SECRET");
if (!apiKey || !configuredSecret) {
  throw new Error("SAMVA_API_KEY and SEND_EMAIL_HOOK_SECRET are required");
}

const samva = createClient({ apiKey });
function normalizeSecret(secret: string): string {
  const trimmed = secret.trim();
  if (trimmed.startsWith("v1,whsec_")) return trimmed.slice("v1,whsec_".length);
  if (trimmed.startsWith("whsec_")) return trimmed.slice("whsec_".length);
  return trimmed;
}

const secret = normalizeSecret(configuredSecret);
const webhook = new Webhook(secret);

Deno.serve(async (request) => {
  const rawBody = await request.text();

  let payload: SendEmailHookPayload;
  try {
    payload = webhook.verify(rawBody, Object.fromEntries(request.headers)) as SendEmailHookPayload;
  } catch {
    return new Response("invalid signature", { status: 401 });
  }

  const rendered = await renderForAction(payload.email_data);

  await samva.messages.send({
    to: [{ email: payload.user.email }],
    channel: "email",
    email: rendered,
  });

  return new Response(null, { status: 200 });
});

Read the body before parsing JSON. The signature covers the raw request body.

Build Supabase confirmation links against the Auth verify endpoint with token_hash, not the six-digit token:

function buildVerifyURL(emailData: EmailData) {
  const params = new URLSearchParams({
    token: emailData.token_hash,
    type: emailData.email_action_type,
    redirect_to: emailData.redirect_to || emailData.site_url,
  });

  return `https://${Deno.env.get("SUPABASE_PROJECT_REF")}.supabase.co/auth/v1/verify?${params}`;
}

reauthentication is OTP-only. email_change may send one or two emails depending on your Secure Email Change setting; follow Supabase's token/hash pairs for the current and new email addresses.

Templates

Render React Email components to HTML and derive a text fallback:

import { render, toPlainText } from "react-email";

const html = await render(<ConfirmSignup url={verifyURL} />);
const text = toPlainText(html);

The Supabase hook only needs small per-action templates. For Tailwind, previews, and reusable email components, see React Email.

Run the complete example

The Supabase Auth hook example includes the full Edge Function, React Email templates, both email_change token/hash pairs, signature fixtures, and tests that mock Samva without sending network requests:

git clone https://github.com/AryaLabsHQ/samva-integrations.git
cd samva-integrations/examples/supabase-auth-hook
bun install
bun run test
bun run typecheck

Copy its environment template, set the three server-only values, and deploy the function with the Supabase CLI.

Failure behavior

Return an empty 200 only after Samva accepts the send. Return 401 for bad signatures and a non-200 response for unsupported email_action_type values or send failures.

The example rejects notification action types and the bare email OTP sign-in type until you add explicit templates for them.

Cookbook and example

For the full signature verification helper, the verify-link builder for each email_action_type, dispatch code covering all six action types (including the two-email email_change case), and the right status codes to return, see the Supabase Auth cookbook. The Supabase Auth hook example is a runnable Edge Function.

On this page