How to test Supabase auth emails on staging without hitting the rate limit

Supabase's built-in sender caps you at a couple of emails per hour and only mails your own team. Here's how to route staging and preview-branch auth emails into a sandbox inbox instead.

• 11 min read •

Your app works locally. Signup fires, the confirmation mail lands in Mailpit, everything checks out. Then you push to a hosted staging project, run the same flow twice, and Supabase hands you `Email rate limit exceeded`. That's the moment most teams start shopping for an SMTP provider. Before you wire a real sender into an environment that gets deleted when the PR merges, ask a different question: does staging need to actually send email, or does it only need to prove it would have? For signup confirmations, magic links, and password resets, the second one is usually enough, and it's a lot safer.

What the built-in Supabase email sender is actually for

Supabase gives every new project a default email service so auth works on day one without any setup. It's deliberately limited, and the limits are documented rather than hidden. Three of them matter for anyone past the demo stage.

  • It's rate limited to 2 messages per hour by default. That's roughly one signup and one password reset before you're locked out for the rest of the hour.
  • It carries no delivery or uptime guarantee. Supabase is explicit that the built-in sender is not covered by an SLA and shouldn't be relied on for anything real.
  • It only delivers to pre-authorized addresses, meaning members of your Supabase organization. Invite an outside tester or use a throwaway address and the message quietly goes nowhere.

That third constraint catches people out more than the rate limit does, because there's no bounce and no error in the client. Your signup call returns success, the user row appears in `auth.users` with `email_confirmed_at` still null, and the tester swears they never got anything. They didn't. The default sender is a demo, not a test environment, and Supabase's own docs treat it that way.

Local dev already has an answer, and it isn't this

If you're running the Supabase CLI, `supabase start` boots a full local stack including Mailpit on port 54324. Every auth email your local project generates lands there, and you open it at http://localhost:54324 to read the HTML, the plain text part, and the headers. Nothing to configure, nothing to sign up for. If your question is purely about local development, stop here and use that.

The gap opens the moment the environment is hosted. A Vercel preview pointed at a Supabase branch, a shared staging project your QA person logs into, a demo instance you send to a client. There's no localhost to catch anything, and the hosted project falls straight back to the built-in sender with its 2-per-hour cap and org-members-only delivery. That's the environment this guide is about.

Why custom SMTP is right for production and wrong for staging

The standard advice is to configure Supabase custom SMTP with a real provider, and for production that advice is correct. Do it. For a staging or preview environment it's the wrong shape for three reasons. First, a real sender really sends. Seed one row from a production dump, let a QA script loop over your users table, and you've just mailed actual customers from a branch called `fix/button-padding`. Second, you get delivery logs instead of the message. Knowing an email was accepted by the provider tells you nothing about whether `{{ .ConfirmationURL }}` rendered against the right host or whether your template collapses in Outlook. Third, the setup cost doesn't amortize. Domain authentication, SPF and DKIM records, sender verification, and per-branch credentials are real work for an environment with a lifespan measured in days.

The three-tier setup, and where each tool fits

Once you split it by environment, the confusion clears up. You're not choosing one tool, you're using three, and each one answers a different question.

Environment Where email goes Tool What you're proving
Local dev Nowhere, captured on your machine Supabase CLI + Mailpit on port 54324 The flow triggers and the template renders
Hosted staging / preview branches Nowhere, captured in a shared inbox Sandbox SMTP (MailSandbox) Real SMTP path works, links point at the right host, whole team can inspect the same message
Production Real inboxes Custom SMTP with a real sender Deliverability, authentication, inbox placement

The middle tier is the one nobody writes about, which is odd, because it's where auth bugs actually surface. Redirect hosts differ from local. Environment variables differ from local. The people testing aren't the people who wrote the code. A sandbox in that slot gives you the real SMTP transaction, a rendered message you can share a link to, and zero chance of a stray send.

Point Supabase's SMTP settings at a sandbox inbox

For a persistent staging project, do this from the dashboard. Open Authentication, then Emails, then SMTP Settings, and enable custom SMTP. Fill in the credentials from your sandbox inbox. The sender address can be anything you like, since no message leaves the sandbox and no DNS record needs to exist for it.

Host: smtp.mailsandbox.com
Port: 2525
Username: <your sandbox inbox username>
Password: <your sandbox inbox password>
Sender email: no-reply@staging.example.com
Sender name: Acme (staging)

Save, then deal with the rate limit while you're in there. Supabase applies its own auth email rate limit on top of whatever SMTP server you point at, and the default once custom SMTP is enabled is 30 messages per hour. That's fine for manual QA and too low for a test suite that walks the whole signup flow on every run. Raise it under Authentication, then Rate Limits, on the setting for sending emails. Since nothing is being delivered, there's no reputation risk in setting it high on a staging project.

The link still works

A sandbox captures the message, it doesn't rewrite it. Copy the confirmation URL out of a captured email, paste it into a browser, and the session is created exactly as it would be for a real user. You're removing delivery, not the auth flow.

Doing the same for Supabase preview branches

Preview branches are ephemeral projects, so clicking through the dashboard for each one defeats the point. Branch configuration comes from `supabase/config.toml` in the branch, which means you set SMTP once in the repo and every branch inherits it.

[auth.email.smtp]
enabled = true
host = "smtp.mailsandbox.com"
port = 2525
user = "env(SANDBOX_SMTP_USER)"
pass = "env(SANDBOX_SMTP_PASS)"
admin_email = "no-reply@staging.example.com"
sender_name = "Acme (staging)"

Keep the credentials in secrets rather than committing them, and remember that the same file drives your local stack. If you want local runs to keep using Mailpit, gate the SMTP block behind your branch config rather than the shared default. One decision worth making up front: one shared sandbox inbox for all branches, or one per branch. Shared is simpler and fine for a small team. Per branch is better once two people are testing signup flows at the same time and can't tell whose confirmation link is whose.

Verify every auth template actually fires

Supabase ships six auth templates: confirm signup, invite user, magic link, change email address, reset password, and reauthentication. Most teams have only ever seen two of them. Now that sending is free of consequences, trigger each one deliberately and read what comes out.

// confirm signup
await supabase.auth.signUp({
email: 'qa+signup@example.com',
password: 'test-1234',
options: { emailRedirectTo: 'https://pr-42.example.vercel.app/auth/callback' }
})
 
// magic link
await supabase.auth.signInWithOtp({ email: 'qa+magic@example.com' })
 
// password reset
await supabase.auth.resetPasswordForEmail('qa+reset@example.com', {
redirectTo: 'https://pr-42.example.vercel.app/auth/reset'
})
 
// change email address (fires two messages, old and new address)
await supabase.auth.updateUser({ email: 'qa+changed@example.com' })

For each captured message, check the things that only break in a hosted environment. Does the confirmation URL point at the preview host, or did it fall back to your Site URL because the redirect wasn't allowed? Is `token_hash` present if you're on the PKCE flow? Does the from header say what you expect, or is it still `noreply@mail.app.supabase.io`? Is there a plain text part, or did a template edit leave you HTML-only? And read the HTML in a client preview, because a magic link button that renders as invisible white-on-white in dark mode is a support ticket you'd rather not file against yourself.

Two things that will still bite you

Routing email to a sandbox fixes delivery, not configuration. The first common failure is the redirect allow list. Supabase only honours a `redirectTo` value that matches an entry under Authentication, then URL Configuration, and preview deployments get a fresh hostname every time. Add a wildcard pattern such as `https://pr-*.example.vercel.app/**` or the link in the email will quietly send everyone to your Site URL and the flow will look broken for reasons that have nothing to do with email.

The second is assuming staging results carry over to production. They don't, and they're not meant to. A sandbox tells you the message was generated correctly, addressed correctly, and renders correctly. Whether Gmail files it under Promotions depends on your production sender, your SPF and DKIM records, and your domain history. Test that in production with a real provider, once, properly, instead of half-testing it on every branch.

Why am I getting "Email rate limit exceeded" on Supabase?

You're still on the built-in email service, which is capped at 2 messages per hour by default. Configuring any custom SMTP server, including a sandbox, moves you off that cap. You can then adjust the auth email rate limit under Authentication, then Rate Limits.

Why did my test user never receive the confirmation email?

The built-in sender only delivers to addresses belonging to members of your Supabase organization. Anything else is accepted and dropped, with no bounce and no client-side error. Custom SMTP removes that restriction.

Does this work with Supabase preview branches?

Yes. Set the SMTP block in supabase/config.toml with credentials pulled from environment secrets, and every branch created from that config picks it up without any dashboard work.

Can I still click the confirmation link if the email never gets delivered?

Yes. The sandbox captures the message as sent, so the confirmation URL and token are intact. Open the captured email, copy the link, and the auth flow completes normally.

Do I need SPF, DKIM, or a verified domain for this?

No. Nothing leaves the sandbox, so there's no domain to authenticate and no sender to verify. You need those for production, not for staging.

Should I use a sandbox in production too?

No. Production needs a real sender with proper authentication. The sandbox covers local-adjacent and staging environments, where accidental delivery is the risk you're managing.

Catch your Supabase auth emails instead of sending them
Create an inbox, drop the SMTP credentials into your Supabase project or config.toml, and every confirmation link, magic link, and password reset from staging lands somewhere you can read it. Free tier, no DNS setup, no risk of mailing a real user from a preview branch.
Get started