Skip to content
Supabase+Resend
Stack Integration

Supabase + Resend: Transactional Emails

For custom transactional emails — booking confirmations, order receipts, notifications — Resend is the cleanest Supabase integration.

Use Cases
  1. Booking confirmation emails triggered by Supabase Realtime
  2. Order receipt emails from a Supabase Edge Function
  3. Custom auth email templates replacing Supabase defaults
  4. Notification digests triggered by Postgres triggers
Implementation

Pattern: Postgres trigger → Supabase Edge Function → Resend API. The Edge Function runs on Deno, calls Resend's API with structured data from the trigger payload.

In detail

How the pieces connect

The data flow has three hops. A row change in Postgres fires a trigger, the trigger calls a Supabase Edge Function over HTTP, and the function hits Resend's REST API to send mail. Each hop runs in a different place: triggers and pg_net (or a webhook via Supabase Database Webhooks) live inside Postgres, the Edge Function runs on Deno at the edge, and Resend handles the actual SMTP delivery and reputation. The cleanest wiring is a Database Webhook on INSERT/UPDATE pointed at the function URL — it passes the changed record and old_record as JSON so you don't query the table again. Inside the function you read Deno.env.get("RESEND_API_KEY") and POST to https://api.resend.com/emails with from, to, subject, and html. Keep the function thin: validate the payload, build the email, send, return 200.

Production gotchas

The non-obvious failures cluster around the boundary between Postgres and the network. First, pg_net calls are fire-and-forget — if the Edge Function errors, the trigger has already committed and the email is silently lost, so log failures in the function and consider a email_outbox table you mark as sent. Second, Resend rate-limits per account; a bulk Postgres UPDATE touching many rows can fan out into hundreds of simultaneous sends, so batch with the /emails/batch endpoint or queue. Third, retries cause duplicates — if a webhook redelivers, the same row sends twice unless you set Resend's idempotency key or dedupe on a stored message id. Finally, the from address must be on a domain you've verified in Resend with SPF and DKIM records, or mail goes to spam. Verify the domain before wiring anything; an unverified sender is the most common 'why no email' cause.

Secrets and security

Never put the Resend API key in client code or in a Postgres trigger string — it belongs only in the Edge Function's environment, set with supabase secrets set RESEND_API_KEY=... and read via Deno.env.get. The function is internet-facing, so guard it: Supabase passes a webhook payload you can validate, but for a public function URL add a shared secret header (checked against another env var) so arbitrary callers can't trigger sends and burn your Resend quota. Keep Row Level Security on the source table — the trigger runs as the table owner regardless, but RLS still protects the data the rest of your app reads. Don't echo user-controlled fields straight into the email html without escaping; a name field with markup becomes an injection vector. Scope the Resend key to send-only where the dashboard allows it.

When this combo fits

This pairing fits when you already run Supabase and want full control over email design and timing — branded receipts, booking confirmations, or digests where Supabase's built-in auth mailer isn't enough. It's a strong fit for replacing default auth emails: point Supabase's custom SMTP at Resend, or send your own via a function for richer templates. It's less ideal when you only need a handful of generic auth emails — the default mailer is simpler — or when volume and segmentation push you toward a full marketing platform with list management and campaign tooling, which Resend deliberately isn't. Pizzeria Bestek runs on React and Supabase, the same backend this guide describes, so a Postgres-trigger-to-Resend path is a natural extension of that stack for order receipts and confirmations.

Live ExamplePizzeria Bestek — Case Study
Other integration guidesView all →
Related

Need this built?