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-refServe Supabase Edge Functions with JWT verification disabled, because the Auth hook fires before a user JWT exists:
supabase functions serve send-email --no-verify-jwtEnabling 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.
Verify links
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.
- Supabase Auth cookbook
- Supabase Auth hook example
- Official Supabase Send Email Hook docs
- Better Auth, if your app uses Better Auth callbacks instead of Supabase Auth hooks.