Samva is in early access — self-serve signup is limited. Have a team invite? Sign up with that email. Contact us for access.

Supabase Auth

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

Supabase Auth

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.

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=sk_sm_...
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 samva = createClient({ apiKey: Deno.env.get("SAMVA_API_KEY")! });
const secret = Deno.env.get("SEND_EMAIL_HOOK_SECRET")!.replace("v1,whsec_", "");
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.

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

The cookbook and example links resolve after the companion samva-integrations PR lands.

On this page