Sending email from Supabase Edge Functions
Supabase Edge Functions run on Deno, which has fetch built in globally, so sending a transactional email doesn't need a dependency at all, just an HTTP call to Envello's API from inside the function.
The function
Create a Supabase Edge Function that sends an email via Envello's API.
- supabase functions new send-email to scaffold the function
- Store the key with supabase secrets set ENVELLO_API_KEY=env_live_..., read it with Deno.env.get('ENVELLO_API_KEY') inside the function
- const res = await fetch('https://api.envello.dev/emails', { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ from, to, subject, html }) })
- Return an appropriate response status from the Edge Function based on res.ok, so whatever called the function (a client, a webhook, a trigger) knows whether the send actually succeeded
Configuration
Use Supabase secrets for sensitive values and environment variables for non-sensitive config.
- supabase secrets set ENVELLO_API_KEY=your_api_key_here
- supabase secrets set ENVELLO_FROM='YourApp <[email protected]>'
- Access with Deno.env.get('ENVELLO_API_KEY') inside the function
- Secrets are encrypted at rest and only available to Edge Functions, not to client-side code
Error handling
Handle API responses explicitly and return meaningful status codes from your Edge Function.
- if (!res.ok): read the error with await res.json()
- 400 errors: return new Response(JSON.stringify({ error: 'Invalid email' }), { status: 400 })
- 429 errors: return new Response(JSON.stringify({ error: 'Rate limited, try again' }), { status: 429 })
- 5xx errors: retry once, then return a 502 if still failing
- Parse the successful response to extract the message ID for logging: const { id } = await res.json()
Triggering from a database event
A common pattern: a Supabase Database Webhook fires the Edge Function on an insert or update (a new signup row, a status change), and the function builds the email content from the row data before calling Envello. This keeps the send logic out of client-side code entirely, which matters since a client-side call would otherwise need to expose your API key.
- Create a Database Webhook in the Supabase dashboard: trigger on INSERT to your users table
- The webhook calls your Edge Function with the new row data in the request body
- In the function: parse the row data, render the email HTML, and call Envello
- This pattern also works for order confirmations (trigger on INSERT to orders), password resets (trigger on INSERT to password_reset_tokens), and status changes (trigger on UPDATE with a filter)
Triggering from a Postgres function
For more control, use a Postgres function + trigger that calls the Edge Function via pg_net (Supabase's HTTP extension for Postgres).
- Create a Postgres function that calls net.http_post() with the Edge Function URL
- Attach a trigger: AFTER INSERT ON users FOR EACH ROW EXECUTE FUNCTION send_welcome_email()
- pg_net runs the HTTP call asynchronously, so the INSERT returns immediately
- This gives you SQL-level control over when emails fire (with WHERE clauses in the trigger, or conditional logic in the function)
Never call this from the client directly
The whole point of routing this through an Edge Function rather than calling Envello's API from a browser or mobile client is keeping the API key server-side. If your Edge Function is triggered by a client request rather than a database event, validate that request server-side (Supabase's row-level security or your own auth check) before sending, rather than letting an unauthenticated request trigger an arbitrary email send.
Validate the user's JWT from the Authorization header: const { data: { user } } = await supabaseClient.auth.getUser(jwt). Return 401 if the token is invalid. Then verify the user has permission to trigger the specific email being requested.
Rendering email HTML
Deno supports template literals natively, which is enough for most transactional emails. For a password reset, it's a heading, a paragraph, and a link.
For more complex layouts, import a lightweight template library from deno.land or npm (via npm: specifier). Keep templates simple: transactional emails that look like a webpage's full design system are harder to maintain and more likely to render inconsistently across email clients.
Webhook handling
Create a separate Edge Function to receive Envello's delivery event webhooks.
- supabase functions new envello-webhook
- Read the raw body: const rawBody = await req.text()
- Verify the HMAC-SHA256 signature using Deno's Web Crypto API
- Parse the event and handle: delivered (log to a Supabase table), bounced (insert into an email_suppressions table), complained (insert into suppressions)
- Return new Response('ok', { status: 200 })
- Set the webhook URL in Envello's dashboard to your Edge Function's URL
Testing locally
Use supabase functions serve to run Edge Functions locally. The local development server supports Deno.env for secrets (set them in a .env.local file that supabase functions serve reads automatically).
Test with curl or a tool like httpie: send a POST request with a sample payload and verify the response. For integration tests, use Envello's test mode so the API accepts requests but doesn't deliver to real inboxes.
Check your domain's SPF, DKIM, and DMARC records
Paste in a domain and see what's missing, plus the exact DNS records to fix it. Free, no account needed.
Check your domain →