Send Email from Supabase: Edge Functions and Custom SMTP for Auth
Two ways to send from Supabase on your own domain: an Edge Function calling the API from a database webhook, and custom SMTP so Auth emails use your domain.
Supabase sends two kinds of email. Auth emails (magic links, confirmations, password resets) go out through Supabase's own default sender with tight rate limits unless you configure custom SMTP, and everything else is yours to send from an Edge Function. Both can come from a mailbox on your own domain. This guide configures the Auth side with SMTP on port 587 and writes an Edge Function that sends through the HTTPS API when a database row changes.
Quick answer
Auth emails: Project Settings, authentication (proof that email really comes from your company), SMTP Settings, enable custom SMTP with host mail.oqumail.com, port 587, the mailbox address as username and sender, and its password. Application emails: supabase secrets set OQUMAIL_API_KEY=oqm_live_..., then an Edge Function that reads Deno.env.get("OQUMAIL_API_KEY") and POSTs to https://api.oqumail.com/api/v1/emails, triggered by a Database Webhook with a shared secret header.
Part 1: Auth emails from your domain
- Create a mailbox such as auth@yourdomain.com in OquMail and note its password.
- In the Supabase dashboard open Project Settings, authentication (proof that email really comes from your company), and under SMTP Settings turn on Enable Custom SMTP. Sender email auth@yourdomain.com, sender name your product, host mail.oqumail.com, port 587, username auth@yourdomain.com, password the mailbox password. Save.
- Under authentication (proof that email really comes from your company), Email Templates, edit the magic link and confirmation templates so the wording and the sender match your product.
- Trigger a password reset from your app and check the message arrives from auth@yourdomain.com with SPF and DKIM passing in Gmail's Show original.
- Keep the Auth rate limits in mind: the mailbox allows 250 sends a day, which covers most early-stage apps; create a second mailbox and key for application mail so Auth never competes with it.
Part 2: an Edge Function for application mail
supabase secrets set OQUMAIL_API_KEY=oqm_live_... WEBHOOK_SECRET=some-long-random-string, then supabase functions new order-confirmation:
// supabase/functions/order-confirmation/index.ts
const ENDPOINT = "https://api.oqumail.com/api/v1/emails";
Deno.serve(async (req) => {
// Database Webhooks send a header you configure; reject anything else.
if (req.headers.get("x-webhook-secret") !== Deno.env.get("WEBHOOK_SECRET")) {
return new Response("forbidden", { status: 403 });
}
const { type, record } = await req.json(); // { type: "INSERT", table, record, ... }
if (type !== "INSERT" || record.status !== "paid") {
return new Response("ignored", { status: 200 });
}
const res = await fetch(ENDPOINT, {
method: "POST",
headers: {
"Authorization": "Bearer " + Deno.env.get("OQUMAIL_API_KEY"),
"Content-Type": "application/json",
},
body: JSON.stringify({
to: [record.customer_email],
subject: "Order " + record.number + " confirmed",
html: "<p>Thanks, your order " + record.number + " is confirmed.</p>",
fromName: "Acme",
}),
});
if (res.status !== 202) {
console.error("send failed", res.status, await res.text());
return new Response("send failed", { status: 502 }); // webhook will not retry; log and alert
}
const receipt = await res.json(); // { id, messageId, status }
return Response.json({ queued: receipt.id });
});Free business email on your own domain
OquMail gives you up to 15 mailboxes on your domain — free — with guided SPF/DKIM/DMARC, webmail, IMAP/SMTP for any mail app, and a send API. Setup is a few DNS records; changes usually take effect within minutes, occasionally up to 48 hours. Start at oqumail.com.
Wire the webhook
- Deploy with supabase functions deploy order-confirmation --no-verify-jwt, since the webhook does not carry a user token.
- In the dashboard open Database, Webhooks, Create. Table orders, event Insert, type HTTP Request, method POST, URL the function URL, and add the header x-webhook-secret with the same value you set as a secret.
- Insert a paid order and watch Edge Function logs for "queued".
- If the function must not fire on every insert, filter inside it (as above) or trigger on Update with a status check; the webhook itself cannot filter rows.
What the API answers
- 202 Accepted: the message is queued. The JSON carries id, messageId and status; store the id so a support question can be matched to the delivery log.
- 400: the body failed validation, usually to missing or not an array, or neither html nor text present.
- 401: the key is missing, revoked or mistyped.
- 403: the mailbox is suspended, has reached its daily limit of 250, or sent more than 10 messages in a minute ("Sending too fast"). Back off; do not retry in a tight loop.
- 422: the message was blocked by content checks; the response explains why. Do not retry unchanged.
- 429: requests arrived faster than the per-key limit. Honour the Retry-After header.
Common questions
Can the Edge Function use SMTP instead?
Deno can open TCP sockets, but Edge Functions have short execution limits and no reason to hold an SMTP session. The HTTPS call is faster and simpler.
Why not use the service role key to send from the client?
The API key must never reach a browser. The Edge Function is the server-side boundary; the client inserts a row, and the function sends.
How do I stop duplicate emails when the webhook fires twice?
Write the receipt id back to the row (an update from the function with the service client) and return early if it is already set; the idempotency guide has the pattern.