Send email from Supabase
A Database Webhook sends a message when a row is inserted, straight from Postgres, with no function to deploy. The Edge Function route when you need to shape the message first.
Updated September 15, 2026
Supabase gives you two places to send from, and the first needs no code at all. A Database Webhook fires an HTTP request when a row is inserted, updated or deleted, and it can carry headers — which is everything a Pigeon Atlas send needs. An Edge Function is for when the message has to be assembled from more than the row.
Email on insert, with no function
In the dashboard, Database → Webhooks → Create a new hook. Pick the table — say orders — and the event, Insert. For the type, choose HTTP Request:
| Field | Value |
|---|---|
| Method | POST |
| URL | https://pigeonatlas.com/v1/emails |
| HTTP Headers | Authorization: Bearer pa_live_your_key_here and Content-Type: application/json |
The webhook posts the whole row as record, which is not the shape the API wants. Give it a body instead. Supabase webhooks are pg_net under the hood, so the honest way to control the body is to write the trigger yourself:
create or replace function public.email_order_confirmation()
returns trigger
language plpgsql
security definer
as $$
begin
perform net.http_post(
url := 'https://pigeonatlas.com/v1/emails',
headers := jsonb_build_object(
'Authorization', 'Bearer ' || current_setting('app.pigeonatlas_key', true),
'Content-Type', 'application/json',
'Idempotency-Key', 'order-' || new.id
),
body := jsonb_build_object(
'from', 'Your Shop <hello@mail.yourshop.com>',
'to', new.customer_email,
'subject', 'Order ' || new.number || ' confirmed',
'text', 'Thanks. Your order ' || new.number || ' is on its way.'
)
);
return new;
end;
$$;
create trigger email_order_confirmation
after insert on public.orders
for each row execute function public.email_order_confirmation();
Set the key once, as a database setting rather than a literal in the function — alter database postgres set app.pigeonatlas_key = 'pa_live_...' — so it is not in every migration and every dump. The Idempotency-Key built from the row id means a replayed trigger cannot send the confirmation twice.
net.http_post is asynchronous: the insert does not wait for us, and a failure shows up in net._http_response, not in your transaction. That is the right behaviour for a receipt — the order should not fail because mail was slow — and the wrong one for a sign-in code, which belongs in a function that can report the error.
When the message needs building
Anything that joins tables, renders a template or decides whether to send at all belongs in an Edge Function. The function is Deno, so use fetch and not SMTP — a function frozen between requests cannot hold a connection open. The Lovable guide has the complete function; it is the same one whether Lovable wrote your app or you did. Store the key with supabase secrets set PIGEONATLAS_API_KEY=... and read it with Deno.env.get.
Auth emails
Supabase Auth's own messages — magic links, confirmations, resets — go out through Supabase's SMTP or one you configure under Authentication → SMTP Settings. Our SMTP endpoint is not yet open, so leave those on Supabase's sender for now and use the routes above for everything your application sends itself.
Before it works
Add mail.yourshop.com in Pigeon Atlas and publish the DKIM, SPF and MX records it shows. Nothing sends from a domain until all three resolve. Use a subdomain rather than the bare domain: a transactional sender should never share a reputation with a newsletter.
A thousand a month free, no card
Enough to verify a domain and run a small app from it.