BLOGS
4 Ways to Send Emails with Supabase (2026)
Four production-ready approaches to sending emails from Supabase: SMTP for auth emails, Edge Functions, database webhooks, auth hooks, and the Node.js SDK.
Motivation
Supabase ships with a built-in email service, but it’s capped at 2 emails per hour and limited to authentication flows. The moment you need a custom welcome email, an order confirmation, or any production-level volume, you need an external provider.
There are four main approaches depending on what you’re sending and how you want to trigger it:
| Approach | Best for | Code required |
|---|---|---|
| SMTP integration | Auth emails (magic links, password resets, confirmations) | None |
| Edge Functions | Custom emails triggered via REST or frontend calls | Moderate |
| Database webhooks | Automatic emails when a row is inserted, updated, or deleted | Moderate |
| Auth hooks | Automatic emails on signup, sign-in, or password reset | Moderate |
If you already run a Node.js backend, you can also use the Node.js server SDK directly — covered at the end of this guide.
All examples use Pingram as the email provider. Here’s a quick comparison of popular providers:
| Provider | Free Tier | Cost per 1K Emails | Notes |
|---|---|---|---|
| Pingram | 3,000/month | $0.40 | Also supports SMS, Slack, push, and more |
| Resend | 3,000/month | $0.40 | Email-focused, requires domain setup |
| SendGrid | 3,000 trial (60 days) | $0.40 | Good for enterprise teams |
| AWS SES | 3,000/mo (12 mo trial) | $0.10 | Complex setup, verification can take weeks |
For a full breakdown, see Best SMTP Providers for Supabase in 2026.
Prerequisites
- A Supabase project with the CLI installed (
npm i -g supabase) - A Pingram account (free tier: 3,000 emails/month)
- Your Pingram API Key (
pingram_sk_...) from the dashboard
1. SMTP Integration for Auth Emails
Supabase auth emails (magic links, password resets, email confirmations) are sent through SMTP. By default they use Supabase’s built-in sender with the 2/hour rate limit. Swapping in Pingram’s SMTP credentials removes that limit and gives you delivery tracking, open/click analytics, and branded sender addresses with zero code changes.
Setup
One-Click: look for the Integrate with Supabase button in our dashboard, available in the onboarding or the Settings page.
Manual:
- In the Pingram dashboard, copy your SMTP credentials
- In the Supabase dashboard, go to Project Settings > Authentication > SMTP Settings
- Toggle Enable Custom SMTP and paste the values from Pingram.
That’s it. All Supabase auth emails now route through Pingram. You can monitor delivery, opens, and bounces from the Pingram Logs page without touching any code.
Also don’t forget to verify your domain in Pingram to send from your own domain.
2. Edge Functions for Custom Emails
Edge Functions are Supabase’s serverless runtime. They run on Deno, deploy globally, and can be called from your frontend, from curl, or from any HTTP client. Use them when you need to send emails from application logic but don’t want to maintain a separate server.
Scaffold a new function (in this example, a function that creates orders on a store):
supabase functions new create-order
Replace the contents of supabase/functions/create-order/index.ts:
import { Pingram } from 'npm:pingram';
const pingram = new Pingram({
apiKey: Deno.env.get('PINGRAM_API_KEY')!
});
Deno.serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers':
'authorization, x-client-info, apikey, content-type'
}
});
}
await pingram.send({
type: 'new_order',
to: { id: 'user_123', email: 'user@example.com' },
email: {
subject: 'You have a new order!',
html: '<h1>You have a new order!</h1>'
}
});
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' }
});
});
The pingram package works in Deno via the npm: specifier — no node_modules needed. The email field lets you set the subject and HTML body inline. You can also use the Pingram dashboard to design templates visually and reference them by type instead.
Call from your frontend
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key'
);
const { data, error } = await supabase.functions.invoke('create-order', {
body: {
order: 'order_123'
}
});
Store your secrets
For local development, create supabase/functions/.env:
PINGRAM_API_KEY=pingram_sk_...
For production:
supabase secrets set PINGRAM_API_KEY=pingram_sk_...
Test locally
supabase start
supabase functions serve create-order --no-verify-jwt --env-file supabase/functions/.env
curl -i --request POST 'http://localhost:54321/functions/v1/send-email'
Deploy
supabase functions deploy create-order
For a deeper walkthrough including CORS handling and advanced patterns, see Send Email from Supabase Edge Functions (Deno, 2026).
3. Database Webhooks
Supabase Database Webhooks call an Edge Function automatically whenever a row is inserted, updated, or deleted. No polling, no cron jobs — the email fires the moment data changes.
Create an Edge Function that parses the webhook payload:
supabase functions new order-notification
import { Pingram } from 'npm:pingram';
import { createClient } from 'npm:@supabase/supabase-js';
const pingram = new Pingram({
apiKey: Deno.env.get('PINGRAM_API_KEY')!
});
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
Deno.serve(async (req) => {
const payload = await req.json();
const order = payload.record;
const { data: user } = await supabase
.from('profiles')
.select('email, first_name')
.eq('id', order.user_id)
.single();
if (!user) {
return new Response(JSON.stringify({ error: 'User not found' }), {
status: 404
});
}
await pingram.send({
type: 'order_confirmation',
to: { id: order.user_id, email: user.email },
email: {
subject: `Order #${order.id} confirmed`,
html: `<h1>Thanks, ${user.first_name}!</h1><p>Your order total: $${order.total}</p>`
}
});
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' }
});
});
The key difference from a regular Edge Function: Supabase sends the webhook payload with a record field containing the row that changed. Your function typically needs to look up related data (like the user’s email) from another table.
Wire the webhook
In the Supabase Dashboard:
- Go to Database > Webhooks
- Click Create a new webhook
- Select the
orderstable and theINSERTevent - Set the type to Supabase Edge Function and choose
order-notification
Deploy the function and every new row in the orders table triggers an email automatically.
4. Auth Hooks
Supabase Auth Hooks run an Edge Function when a user signs up, signs in, or resets their password. Use them for branded welcome emails, onboarding sequences, or any logic that should fire on auth events.
supabase functions new auth-welcome
import { Pingram } from 'npm:pingram';
const pingram = new Pingram({
apiKey: Deno.env.get('PINGRAM_API_KEY')!
});
Deno.serve(async (req) => {
const payload = await req.json();
const { user } = payload;
const firstName = user.user_metadata?.first_name ?? 'there';
await pingram.send({
type: 'welcome',
to: { id: user.id, email: user.email },
email: {
subject: 'Welcome!',
html: `<h1>Hey ${firstName}, welcome aboard!</h1>`
}
});
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' }
});
});
The payload shape is different from database webhooks — it contains a user object from the auth event instead of a table row.
Enable the hook
In the Supabase Dashboard, go to Authentication > Hooks and point it at your auth-welcome function. Deploy with:
supabase functions deploy auth-welcome
Alternative: Node.js Server SDK
If you already run a Node.js backend (Express, Fastify, Next.js API routes), you can send emails directly from your server without Edge Functions.
Install
npm install pingram
Node Example
const { Pingram } = require('pingram');
const pingram = new Pingram({
apiKey: process.env.PINGRAM_API_KEY
});
await pingram.send({
type: 'welcome',
to: { id: 'user_123', email: 'user@example.com' },
email: {
subject: 'Welcome!',
html: '<h1>Welcome to our app!</h1>'
}
});
You can also listen for Supabase real-time changes from Node.js using the Supabase client library:
const { createClient } = require('@supabase/supabase-js');
const { Pingram } = require('pingram');
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
);
const pingram = new Pingram({
apiKey: process.env.PINGRAM_API_KEY
});
supabase
.channel('orders')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'orders' },
async (payload) => {
const order = payload.new;
const { data: user } = await supabase
.from('profiles')
.select('email, first_name')
.eq('id', order.user_id)
.single();
if (user) {
await pingram.send({
type: 'order_confirmation',
to: { id: order.user_id, email: user.email },
email: {
subject: `Order #${order.id} confirmed`,
html: `<h1>Thanks, ${user.first_name}!</h1><p>Your order total: $${order.total}</p>`
}
});
}
}
)
.subscribe();
Which Approach Should You Use?
- Use SMTP integration if you only need to fix Supabase’s auth email rate limit. No code, 2-minute setup.
- Use Edge Functions if you want to send custom emails from your frontend or API calls without maintaining a separate server.
- Use database webhooks if you want emails to fire automatically when data changes — no client-side code needed.
- Use auth hooks if you want branded welcome emails or custom logic on signup/sign-in events.
- Use the Node.js SDK if you already have a backend and prefer to keep email logic there.
You can combine multiple approaches in the same project — SMTP for auth emails, database webhooks for order notifications, and Edge Functions for everything else.
Common Issues and Fixes
Supabase auth emails coming from a Pingram domain
By default, emails are sent from a Pingram domain. To send from your own domain, verify it in the Pingram dashboard under Settings > Domains. This also improves deliverability.
”Module not found” in Edge Functions
Deno’s npm: specifier requires Supabase CLI v1.50+:
npm i -g supabase@latest
Not receiving emails
Check the Pingram Logs page. If the email appears in logs but isn’t arriving, look at the log detals.
If the email doesn’t appear in logs at all, your API key or region is likely misconfigured. Make sure you’re initializing with the correct region:
const pingram = new Pingram({
apiKey: 'pingram_sk_...',
region: 'us' // or 'eu', 'ca' (US is default and not required)
});