All notes

Supabase · October 3, 2026

Send from a Supabase project

A Supabase Edge Function calls the Pigeon API when a row is inserted. The service role stays in Supabase. The Pigeon key stays in function secrets.

1. Prepare Pigeon

  1. Verify the domain you will use in From.
  2. Create a template with the slug welcome, publish it, and leave a {{email}} variable where the address should appear.
  3. Create a sending API key and copy it once.

2. Store the secrets

In the Supabase project, open Edge Functions, then Secrets. Use https://pigeonfs.com when the function should call the hosted API. Use http://host.docker.internal:4005 when the function runs against Pigeon on your machine.

shell
supabase secrets set \
  PIGEON_URL=https://pigeonfs.com \
  PIGEON_API_KEY=pg_your_key \
  PIGEON_FROM="Ada <ada@yourdomain.com>"

3. Deploy the welcome function

Create supabase/functions/welcome/index.ts. A copy of this handler lives in examples/supabase/welcome in the Pigeon repo. Deploy it, then add a Database Webhook that runs the function when a row is inserted into auth.users or your profiles table.

shell
supabase functions deploy welcome --no-verify-jwt

Use --no-verify-jwt only for a database webhook that cannot send a user JWT. Check a shared secret header inside the function if the URL could be called by anyone else.

JavaScript
import "jsr:@supabase/functions-js/edge-runtime.d.ts";

Deno.serve(async (req) => {
  const payload = await req.json();
  const email = payload.record?.email;
  if (!email) return new Response("missing email", { status: 400 });

  const response = await fetch(Deno.env.get("PIGEON_URL") + "/api/emails", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${Deno.env.get("PIGEON_API_KEY")}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      from: Deno.env.get("PIGEON_FROM"),
      to: [email],
      template: "welcome",
      variables: { email }
    })
  });

  return new Response(await response.text(), { status: response.status });
});

Use case: welcome the person who just signed up

The webhook body includes record.email. The function posts that address to Pigeon with the welcome template. Open Emails in Pigeon and look for a sent or delivered row. A 401 means the key does not match. A 403 domain_not_verified means From is still pending.

Use case: invite someone to a team

Point a second webhook at the same shape of function when a row is inserted into team_invites. Read record.email and record.team_name, and send a template slug of team-invite with those variables. Keep a different sending key if you want the invite mail in its own log.

Do the domain setup from the editor