Send email from a Supabase Edge Function
Trigger a Sendry email from a Deno-based Supabase Edge Function — full code, environment, and deploy walkthrough.
Supabase Edge Functions run Deno on the edge and are the canonical place to put server-side logic in a Supabase stack. This recipe shows how to call Sendry's REST API directly from an Edge Function — useful for welcome emails, receipts, on-demand notifications, or anywhere you'd otherwise reach for a Postgres trigger + cron.
What you'll build
An Edge Function send-welcome that:
- Receives an event (signup, order, etc.) over HTTPS.
- Validates a shared secret to prevent abuse.
- Calls
POST https://api.sendry.online/v1/emailsdirectly (no SDK — keeps the Deno bundle tiny).
Prerequisites
- Supabase CLI installed (
supabase --version). - Sendry API key with
sending_accessscope. - At least one verified sending domain in Sendry.
Step 1: Scaffold the function
supabase functions new send-welcome
That creates supabase/functions/send-welcome/index.ts.
Step 2: Implement the function
// supabase/functions/send-welcome/index.ts
// deno-lint-ignore-file no-explicit-any
const SENDRY_API_KEY = Deno.env.get("SENDRY_API_KEY")!;
const FUNCTION_SECRET = Deno.env.get("FUNCTION_SECRET")!;
const FROM = "Acme <hello@acme.com>";
interface RequestBody {
email: string;
first_name?: string;
}
Deno.serve(async (req) => {
if (req.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
// Shared-secret check — Supabase's built-in JWT is also an option,
// but a custom header is easier to call from external systems.
if (req.headers.get("x-function-secret") !== FUNCTION_SECRET) {
return new Response("Unauthorized", { status: 401 });
}
let body: RequestBody;
try {
body = (await req.json()) as RequestBody;
} catch {
return Response.json({ error: "invalid_json" }, { status: 400 });
}
if (!body.email) {
return Response.json({ error: "missing_email" }, { status: 400 });
}
const firstName = body.first_name ?? "there";
const resp = await fetch("https://api.sendry.online/v1/emails", {
method: "POST",
headers: {
authorization: `Bearer ${SENDRY_API_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
from: FROM,
to: body.email,
subject: `Welcome to Acme, ${firstName}!`,
html: `
<h1>Welcome, ${escapeHtml(firstName)}!</h1>
<p>Thanks for joining Acme. Your account is ready — sign in to get started.</p>
<p><a href="https://acme.com/sign-in">Sign in</a></p>
`,
text: `Welcome, ${firstName}! Sign in: https://acme.com/sign-in`,
tags: [
{ name: "kind", value: "lifecycle" },
{ name: "action", value: "welcome" },
],
}),
});
if (!resp.ok) {
const err = await resp.text();
console.error("sendry error", resp.status, err);
return Response.json(
{ error: "sendry_error", status: resp.status, body: err },
{ status: 502 },
);
}
const { id } = (await resp.json()) as { id: string };
return Response.json({ ok: true, id });
});
function escapeHtml(s: string): string {
return s
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
Step 3: Set the secrets
supabase secrets set SENDRY_API_KEY=sn_live_xxxxxxxxxxxx
supabase secrets set FUNCTION_SECRET=$(openssl rand -hex 32)
The FUNCTION_SECRET is what callers will pass in x-function-secret.
Step 4: Deploy
supabase functions deploy send-welcome --no-verify-jwt
We pass --no-verify-jwt because we're using our own shared secret. If you want callers to authenticate with a Supabase user JWT instead, drop that flag and use getUser inside the function.
Step 5: Call it
curl -X POST https://<project-ref>.supabase.co/functions/v1/send-welcome \
-H "x-function-secret: $FUNCTION_SECRET" \
-H "content-type: application/json" \
-d '{ "email": "jane@example.com", "first_name": "Jane" }'
Or from the client SDK:
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
await supabase.functions.invoke("send-welcome", {
body: { email: "jane@example.com", first_name: "Jane" },
headers: { "x-function-secret": FUNCTION_SECRET },
});
Variants
Send from a Postgres webhook
In Supabase Studio → Database → Webhooks, create a webhook on INSERT into auth.users that POSTs to https://<project-ref>.supabase.co/functions/v1/send-welcome with the x-function-secret header. Now every signup fires your welcome email automatically — no client-side code required.
Use the Sendry SDK instead of fetch
If you prefer the typed SDK, Deno can import it from npm:
import { Sendry } from "npm:sendry-sdk";
const sendry = new Sendry(Deno.env.get("SENDRY_API_KEY")!);
await sendry.emails.send({ from: FROM, to: body.email, subject, html });
The bundle is larger but the ergonomics are better, especially for batch sends, contact upserts, or campaign triggers.
Troubleshooting
401 Unauthorized: confirmx-function-secretmatches whatsupabase secrets listshows. Edge Functions cold-start with the latest secrets, so a redeploy is unnecessary aftersecrets set.Domain not verifiedfrom Sendry: make sure thefrom:domain inFROMis actually verified in Sendry → Domains.- CORS errors when calling from the browser: add an
OPTIONSpreflight handler that returns the appropriateaccess-control-allow-*headers, or invoke viasupabase.functions.invoke()which handles CORS for you.
Supabase Auth emails via Sendry
Replace Supabase's default Auth emails (password reset, magic link, signup confirmation) with Sendry using Send Email Hooks.
Sync Supabase auth users into a Sendry audience
Use a Postgres trigger + database function to automatically add new Supabase auth users into a Sendry audience, with backfill for existing users.