BLOGS

Send Email from Supabase Edge Functions (Deno, 2026)

March 11, 2026

Step-by-step guide to sending emails from Supabase Edge Functions using Deno and the Pingram SDK — on-demand, database webhook, and auth hook patterns.

Send Email from Supabase Edge Functions (Deno, 2026)

Motivation

Supabase’s built-in emails are limited to authentication flows. Edge Functions let you send any email you want — welcome emails, order confirmations, alerts — without that limit and without maintaining a separate server.

Edge Functions run on the Deno runtime inside Supabase’s infrastructure. They deploy globally and can be triggered three ways:

  1. On-demand — called from your frontend or any HTTP client
  2. Database webhooks — fired automatically when a row changes
  3. Auth hooks — fired on signup, sign-in, or password reset

This guide walks through all three patterns using the Pingram SDK. For a broader overview of all email approaches in Supabase (including SMTP and Node.js), see How to Send Emails with Supabase.

Prerequisites

1. Create Your First Edge Function

Scaffold a new function:

supabase functions new send-email

This creates supabase/functions/send-email/index.ts. Replace the contents with:

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: 'welcome',
    to: { id: 'user_123', email: 'user@example.com' },
    email: {
      subject: 'Welcome!',
      html: '<h1>Welcome to our app!</h1>'
    }
  });

  return new Response(JSON.stringify({ success: true }), {
    headers: { 'Content-Type': 'application/json' }
  });
});

What’s happening here

2. Store Your Secrets

Never hard-code API keys. Supabase provides two ways to manage secrets:

Local development

Create a .env file at supabase/functions/.env:

PINGRAM_API_KEY=pingram_sk_...

This file is loaded automatically when you run supabase functions serve.

Production

Push secrets via the CLI:

supabase secrets set PINGRAM_API_KEY=pingram_sk_...

Verify they’re set:

supabase secrets list

3. Test Locally

Start the local Supabase stack and serve the function:

supabase start
supabase functions serve send-email --no-verify-jwt --env-file supabase/functions/.env

Send a test request:

curl -i --request POST 'http://localhost:54321/functions/v1/send-email'

Check your inbox. If the email doesn’t appear, see the troubleshooting section below.

4. Call from Your Frontend

Use the Supabase client library to invoke the function from any frontend framework:

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  'https://your-project.supabase.co',
  'your-anon-key'
);

const { data, error } = await supabase.functions.invoke('send-email');

The supabase-js client handles auth headers automatically, so your Edge Function receives the user’s JWT. You can verify the token inside the function if you want to restrict access.

5. Trigger Emails from Database Changes

Supabase Database Webhooks call your Edge Function automatically whenever a row is inserted, updated, or deleted. No polling, no cron jobs.

Example: send a notification when a new order is created

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 the on-demand pattern: 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:

  1. Go to Database > Webhooks
  2. Click Create a new webhook
  3. Select the orders table and the INSERT event
  4. Set the type to Supabase Edge Function and choose order-notification

Every new row in the orders table now triggers an email automatically.

6. Send Emails on Auth Events

Supabase Auth Hooks run an Edge Function when a user signs up, signs in, or resets their password. Use them for branded welcome emails that go beyond the default Supabase auth templates.

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 under Authentication > Hooks and point it at your auth-welcome function.

7. Deploy to Production

Once you’ve verified everything locally, deploy your functions:

supabase functions deploy send-email
supabase functions deploy order-notification
supabase functions deploy auth-welcome

Make sure your production secrets are already set (Step 2). Each function deploys independently and scales automatically across Supabase’s global edge network.

Common Issues and Fixes

Email received from 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” for npm packages

Deno’s npm: specifier requires Supabase CLI v1.50+. Update with:

npm i -g supabase@latest

CORS errors when calling from the browser

Ensure your function handles OPTIONS requests and returns the appropriate Access-Control-Allow-Origin header (see the code in Step 1).

Not receiving emails

Check the Pingram Logs page. If the email appears in logs but isn’t arriving, look at the log details.

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'
});

Edge Function timeouts

Edge Functions have a default timeout of 60 seconds. Most operations and API calls should resolve in seconds, but if you’re doing heavy processing before sending, consider splitting the work into multiple functions.

LLM tools take very long. It’s recommended that they run as background jobs, so they don’t block the operation of the function itself.

Resources