DEV Community

Hani Khanani
Hani Khanani

Posted on Originally published at ownmailserver.com

How to set up Supabase custom SMTP with OwnMailServer

Every production Supabase project needs custom SMTP, because the built-in sender only emails your own team and allows 2 messages an hour. To send through OwnMailServer, open your project's Authentication settings, go to SMTP, and enter smtp.ownmailserver.com, port 587, any username and an API key as the password, with a From address on your verified domain. Then raise Supabase's 30-an-hour starting limit to match your volume. It takes about ten minutes.

Approach Good for Code
Custom SMTP Most projects: Supabase's templates, sent from your domain None
Send Email Hook + our API Your own templates, languages per user, logging One Edge Function

Why Supabase needs custom SMTP

Supabase Auth sends email for signup confirmation, magic links, password resets, invitations, email changes and reauthentication. Out of the box it uses a shared sender that, as of October 2026, Supabase documents like this:

  • It only delivers to addresses in your organization's Team settings. Anyone else gets Email address not authorized.
  • It allows 2 messages an hour, and Supabase says this can change without notice.
  • It has no delivery or uptime guarantee.

That's enough to try Supabase, not to run an app. With custom SMTP, sign-in emails come from your domain, signed with your DKIM key, and every bounce and complaint shows up in your dashboard.

What you need before you start

  • An approved OwnMailServer account with a verified sending domain. The From address must be on that domain, or we refuse the email with 550. The API docs list the DNS records, and the DKIM guide explains them.
  • An API key, created in your dashboard under API keys & SMTP. Choose Sending only (the default), limited to your sending domain, so the key Supabase stores can't do anything but send. Make one just for Supabase, so you can revoke it without touching your other apps.
  • Ideally a separate From address or subdomain for auth email, such as no-reply@auth.acme.com. Supabase recommends this too: if a marketing campaign draws complaints, your password resets aren't caught up in it.

Step 1: Enter the SMTP settings

In the Supabase dashboard, open your project, then Authentication, then the SMTP settings, and turn on custom SMTP:

Field Value
Sender email no-reply@auth.acme.com (on your verified domain)
Sender name Your app's name, such as Acme
Host smtp.ownmailserver.com
Port 587
Username ownmailserver (we accept any value)
Password Your API key

Save. From now on Supabase can email any address. Supabase connects on 587, upgrades with STARTTLS and logs in, which is what we require.

To set the same thing from a script or CI, use Supabase's Management API with an access token from your account settings:

curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "external_email_enabled": true,
    "smtp_admin_email": "no-reply@auth.acme.com",
    "smtp_sender_name": "Acme",
    "smtp_host": "smtp.ownmailserver.com",
    "smtp_port": "587",
    "smtp_user": "ownmailserver",
    "smtp_pass": "'"$OMS_API_KEY"'"
  }'
Enter fullscreen mode Exit fullscreen mode

Step 2: Raise the rate limit, in step with warm-up

When you turn on custom SMTP, Supabase sets a deliberately low limit: 30 auth emails an hour. Change it on the Rate Limits page in the Authentication settings.

Two limits now apply, and the lower one wins:

  • Supabase's hourly limit, which you set.
  • Our daily warm-up cap. A new account starts at a few hundred recipients a day and rises about 25% after each clean day (how warm-up works). Your dashboard shows today's cap.

If Supabase allows more than our cap, we answer with a temporary 450 and Supabase reports a sending error. So raise Supabase's limit to roughly your cap divided by the hours your users are active, and raise it again as the cap grows. Auth email is usually low volume, so most projects never get close.

Step 3: Test it

Sign up with an address that isn't on your Supabase team, or trigger a reset:

const { error } = await supabase.auth.resetPasswordForEmail("jane@example.com", {
  redirectTo: "https://acme.com/update-password",
});
Enter fullscreen mode Exit fullscreen mode

Then search for the address in your OwnMailServer dashboard to see the delivery, or ask the Claude connector what happened to it. If nothing arrived:

You see Cause Fix
Supabase: Email address not authorized Custom SMTP isn't saved or enabled Save the SMTP settings again
535 / authentication failed Password isn't a valid API key Paste the full key, or create a new one
550 Sender domain isn't verified, or the account is pending Finish domain setup in the dashboard
450 Warm-up cap or 10 a second reached Wait, or lower Supabase's rate limit
Supabase: rate limit exceeded Supabase's own hourly limit Raise it under Rate Limits

After 5 failed logins from one IP address we block that address for 15 minutes, so fix a wrong password before retrying many times.

Step 4: Edit the templates

Supabase writes these emails, so you change the wording under Authentication, then Email Templates: confirm signup, invite, magic link, change email address, reset password and reauthentication. Templates use Go template variables: put .ConfirmationURL or .Token inside double curly braces.

Keep auth emails plain: a short line, one button, the same link as text, and no tracking or marketing. That's what mailbox providers expect from a password reset, and it's what gets it to the inbox quickly.

Option B: A Send Email Hook that calls our API

If you want your own templates, per-user languages or a record of each send in your database, use Supabase's Send Email Hook. When the hook is on, Supabase stops using SMTP and calls your endpoint for every auth email. You build the email and send it through POST /emails.

Create an Edge Function and its secrets. The hook secret comes from Authentication, then Hooks, when you add a Send Email Hook pointing to the function's URL:

supabase functions new send-email
supabase secrets set OMS_API_KEY=... SEND_EMAIL_HOOK_SECRET="v1,whsec_..."
Enter fullscreen mode Exit fullscreen mode
// supabase/functions/send-email/index.ts
import { Webhook } from "https://esm.sh/standardwebhooks@1.0.0";

const hookSecret = Deno.env.get("SEND_EMAIL_HOOK_SECRET")!.replace("v1,whsec_", "");
const PROJECT_REF = "your-project-ref";

const subjects: Record<string, string> = {
  signup: "Confirm your email",
  magiclink: "Your sign-in link",
  recovery: "Reset your password",
  invite: "You've been invited",
  email_change: "Confirm your new email address",
  reauthentication: "Your confirmation code",
};

Deno.serve(async (req) => {
  const payload = await req.text();
  const headers = Object.fromEntries(req.headers);
  let user, email_data;
  try {
    ({ user, email_data } = new Webhook(hookSecret).verify(payload, headers) as any);
  } catch {
    return new Response(JSON.stringify({ error: { http_code: 401, message: "Invalid signature" } }), { status: 401 });
  }

  const link = `https://${PROJECT_REF}.supabase.co/auth/v1/verify?` + new URLSearchParams({
    token: email_data.token_hash,
    type: email_data.email_action_type,
    redirect_to: email_data.redirect_to,
  });

  const res = await fetch("https://api.ownmailserver.com/emails", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${Deno.env.get("OMS_API_KEY")}`,
      "Content-Type": "application/json",
      // The same hook call retried won't send twice.
      "Idempotency-Key": `supabase-${email_data.token_hash}`,
    },
    body: JSON.stringify({
      from: "Acme <no-reply@auth.acme.com>",
      to: [user.email],
      subject: subjects[email_data.email_action_type] ?? "Message from Acme",
      html: `<p><a href="${link}">Continue</a></p><p>Or enter this code: ${email_data.token}</p>`,
      text: `Continue: ${link}\nOr enter this code: ${email_data.token}`,
    }),
  });
  if (!res.ok) {
    const body = await res.json();
    return new Response(JSON.stringify({ error: { http_code: res.status, message: body.message } }), { status: 500 });
  }
  return new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } });
});
Enter fullscreen mode Exit fullscreen mode
supabase functions deploy send-email --no-verify-jwt
Enter fullscreen mode Exit fullscreen mode

Deploy with --no-verify-jwt: Supabase Auth signs the hook call with the hook secret, not a user's JWT, and the function checks that signature itself.

Two details from Supabase's docs to handle before you rely on it. With Secure Email Change on, an email_change call carries two token pairs, one for each address, and Supabase notes the field names are reversed from what you'd expect, so follow its hook docs for that case. Newer versions of the hook can also send security notifications, such as password_changed_notification; give those their own subject and text rather than the fallback above.

When this isn't the right fit

If your project only sends a handful of auth emails, any SMTP provider with a free tier will do, and Supabase's limits are the real constraint, not cost. We're a good fit when you want auth and app email from one provider on your own domain, with bounces, complaints and warm-up handled for you. The Firebase guide covers the same setup for Firebase, and pay-as-you-go pricing is $0.20 per 1,000 emails with a $10 monthly minimum.

FAQ

Why does Supabase say "Email address not authorized"?

Your project is still using Supabase's built-in email sender, which only delivers to members of your Supabase organization's team. Any other address gets that error. Set up custom SMTP in your project's Authentication settings and Supabase can email anyone. The built-in sender is meant for trying Supabase out, and Supabase says it has no delivery guarantee.

How do I raise Supabase's email rate limit?

Set up custom SMTP first: the built-in sender is fixed at 2 emails an hour. After that, Supabase starts you at 30 emails an hour, which you can raise on the project's Rate Limits page in the Authentication settings. Raise it in steps that match your warm-up cap with us, so the extra email isn't refused during your first weeks.

What SMTP port should I use for Supabase?

Use 587. Supabase connects, upgrades the connection with STARTTLS and then logs in, which is what we require. Port 25 isn't for authenticated sending, and we don't offer 465. If Supabase reports a login error, the password is usually wrong: it must be a full API key from your OwnMailServer dashboard.

Can I change the text of Supabase auth emails?

Yes. Supabase's Email Templates page lets you edit the subject and HTML of each email, such as confirm signup, magic link and reset password, and custom SMTP sends whatever you write there. For full control, such as your own template system or languages per user, use a Send Email Hook and build each email yourself.

Should Supabase auth emails use a separate domain?

It helps. Supabase recommends separate sending domains or From addresses for auth and marketing email, such as auth.example.com and marketing.example.com. Mailbox providers judge each domain's reputation, so a newsletter that draws complaints won't drag your password resets into spam. With us, verify the subdomain as its own sending domain and use it as Supabase's sender address.


Originally published at ownmailserver.com.

Top comments (0)