Send email with Supabase Edge Functions

Deno on Supabase: send when a row changes, straight from the database.

You need a verified sending domain and an API key from Settings › API Keys. If neither exists yet, the quickstart covers both in a couple of minutes.

Send

Deno.serve(async (req) => {
  const { record } = await req.json();          // database webhook payload
  const res = await fetch("https://sending.dev/api/v1/emails", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${Deno.env.get("SENDING_API_KEY")}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      from: "Acme <[email protected]>",
      to: record.email,
      subject: "Welcome",
      html: "<p>You're in.</p>",
      // The row id makes the send idempotent even if the trigger fires twice.
      idempotencyKey: `welcome-${record.id}`,
    }),
  });
  return new Response(await res.text(), { status: res.status });
});

A successful call answers 202 with the message id: accepted and queued, not yet delivered. Delivery, opens, bounces and complaints arrive later, on webhooks or in the dashboard.

What changes in Supabase Edge Functions

Set the secret with supabase secrets set SENDING_API_KEY=..., then point a database webhook at the function on insert.

The thing that catches people

A database webhook can fire more than once for the same row, and Supabase retries on failure. The row id as the idempotency key is what keeps that from becoming duplicate mail.

Next

  • All the fields: cc and bcc, reply-to, attachments, templates, scheduling.
  • Webhooks: delivery, bounce and complaint events, signed with HMAC.
  • Domains: SPF, DKIM and MAIL FROM, and why sending is refused until they are in place.
  • MCP: the same operations as tools, when the one writing the code is an agent.

Nearby: Platforms and edge runtimes

All 30 stacks