Article Preview
Sending Email with Supabase Edge Functions and Mailgun
This guide shows you how to send transactional email using Supabase Edge Functions and the Mailgun API. Edge Functions run on Deno and provide a serverless way to integrate with Mailgun without managing backend infrastructure.
Prerequisites
- A Supabase account with a project created
- A Mailgun account with a verified domain
- Your Mailgun API key and domain from the Mailgun Dashboard
- Supabase CLI installed locally
What are Supabase Edge Functions?
Supabase Edge Functions are serverless TypeScript functions that run on the Deno runtime at the edge, close to your users. They're perfect for integrating with third-party APIs like Mailgun because they:
- Start instantly with no cold starts
- Run on a secure, isolated runtime
- Handle CORS automatically with simple configuration
- Support environment variables for API keys
Building Your First Email Function
Step 1: Set Up CORS Headers
Create a shared CORS configuration that all your functions can use:
File: supabase/functions/_shared/cors.ts
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}Step 2: Create the Edge Function
Create your edge function with the basic structure for sending email. This example focuses on the four essential fields: from, to, subject, and text.
File: supabase/functions/send-mailgun-email/index.ts
import { corsHeaders } from '../_shared/cors.ts'
Deno.serve(async (req) => {
// Handle CORS preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
// 1. Parse the request body
const { from, to, subject, text } = await req.json()
// 2. Get Mailgun credentials from environment
const apiKey = Deno.env.get('MAILGUN_API_KEY')
const domain = Deno.env.get('MAILGUN_DOMAIN')
if (!apiKey || !domain) {
throw new Error('Missing Mailgun credentials')
}
// 3. Build the Mailgun API endpoint
const url = `https://api.mailgun.net/v3/${domain}/messages`
// 4. Create FormData (Mailgun requires FormData, not JSON)
const formData = new FormData()
formData.append('from', from)
formData.append('to', to)
formData.append('subject', subject)
formData.append('text', text)
// 5. Send the email via Mailgun API
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`api:${apiKey}`)}`,
},
body: formData,
})
if (!response.ok) {
throw new Error(`Mailgun API error: ${response.statusText}`)
}
const data = await response.json()
// 6. Return the response
return new Response(
JSON.stringify(data),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 202,
},
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
},
)
}
})Step 3: Configure Environment Variables
Set your Mailgun credentials as Supabase secrets:
supabase secrets set MAILGUN_API_KEY=key-your-api-key-here
supabase secrets set MAILGUN_DOMAIN=mg.yourdomain.comStep 4: Deploy the Function
Deploy your function to Supabase:
supabase functions deploy send-mailgun-emailStep 5: Test Your Function
Send a test email using curl:
curl -i --location --request POST \
'https://your-project-ref.supabase.co/functions/v1/send-mailgun-email' \
--header 'Authorization: Bearer YOUR_ANON_KEY' \
--header 'Content-Type: application/json' \
--data '{
"from": "hello@yourdomain.com",
"to": "user@example.com",
"subject": "Welcome!",
"text": "Thanks for signing up!"
}'Key Concepts Explained
Why FormData Instead of JSON?
Mailgun's Messages API requires FormData encoding, not JSON. This is why we create a FormData object and append fields to it:
const formData = new FormData()
formData.append('from', from)
formData.append('to', to)
formData.append('subject', subject)
formData.append('text', text)Authentication with Mailgun
Mailgun uses HTTP Basic Authentication. The username is always api, and the password is your API key:
headers: {
'Authorization': `Basic ${btoa(`api:${apiKey}`)}`,
}The btoa() function encodes the credentials in Base64, which is required for Basic Auth.
Why 202 Status Code?
Mailgun returns a 202 (Accepted) status code because the email is queued for delivery, not immediately sent. This is standard for transactional email services that handle email asynchronously.
Advanced Features
This basic example gets you started, but you may want to add:
-
HTML emails - Add an
htmlfield alongside or instead oftext - CC and BCC recipients - Include additional recipients
- Reply-To headers - Set a different reply address
- Input validation - Validate email addresses and required fields
- Multiple recipients - Send to comma-separated email lists
-
EU region support - Use
api.eu.mailgun.netfor EU customers
For a production-ready implementation with all these features, see our complete example on GitHub.
Local Development
To test your function locally before deploying:
-
Create a
.env.localfile:MAILGUN_API_KEY=key-xxxxx MAILGUN_DOMAIN=mg.example.com -
Start the local Supabase stack:
supabase start -
Serve your function:
supabase functions serve send-mailgun-email --env-file .env.local -
Test against localhost:
curl -i --location --request POST \ 'http://127.0.0.1:54321/functions/v1/send-mailgun-email' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ --header 'Content-Type: application/json' \ --data '{"from":"test@yourdomain.com","to":"user@example.com","subject":"Test","text":"Hello!"}'
Troubleshooting
"Missing Mailgun credentials" Error
Make sure you've set both environment variables:
supabase secrets set MAILGUN_API_KEY=key-xxxxx
supabase secrets set MAILGUN_DOMAIN=mg.yourdomain.comAfter setting secrets, redeploy the function:
supabase functions deploy send-mailgun-email"Unauthorized" from Mailgun
- Verify your API key is correct in the Mailgun Dashboard
- Ensure you're using the correct domain (the one shown in Mailgun, like
mg.yourdomain.com) - Check that your domain is verified in Mailgun
"Forbidden" or Domain Errors
- Make sure you're using a verified sending domain in the
fromfield - For sandbox domains, you must add recipients as authorized recipients in Mailgun
Using EU Region
If your Mailgun account is in the EU region, change the API endpoint in your code from:
const url = `https://api.mailgun.net/v3/${domain}/messages`To:
const url = `https://api.eu.mailgun.net/v3/${domain}/messages`Check your Mailgun Dashboard URL to confirm your region: US accounts use app.mailgun.com, EU accounts use app.eu.mailgun.com. The full example handles both regions automatically.
Next Steps
- Add email templates - Use Mailgun's template features for consistent branding
- Track email events - Set up webhooks to receive delivery, open, and click events
- Handle bounces - Implement bounce handling with Mailgun's suppression lists
- Send at scale - Use batch sending for newsletters or bulk notifications
Resources
- Full Production Example - Complete implementation with validation, error handling, and advanced features
- Mailgun API Documentation
- Supabase Edge Functions Docs
- Mailgun Dashboard