Supabase Auth sends sign-up confirmations, magic links and one-time codes, invitations, email change and password reset messages. Out of the box they go through a built-in service meant for testing: it only delivers to your project's team, sends a couple of messages an hour, and every message comes from Supabase's address, not yours. Custom SMTP replaces it for all of these.
| Setting | Value |
|---|---|
| SMTP host | smtp.postwing.app |
| Port | 587 |
| Encryption | STARTTLS (the connection is upgraded to TLS before login) |
| Username | The login of an SMTP token for your domain |
| Password | The password of that token — shown once, when the token is created |
In the Supabase dashboard, open Authentication → Emails → SMTP Settings, turn on Enable Custom SMTP and fill in:
| Field | Value |
|---|---|
| Sender email | noreply@your-domain.com — on your verified domain |
| Sender name | Your app's name |
| Host | smtp.postwing.app |
| Port number | 587 |
| Username | The login of an SMTP token for your domain |
| Password | That token's password |
Supabase advises a sending domain used only for authentication — say auth.your-domain.com — kept apart from marketing mail, so a campaign's complaints never slow down a password reset. Add it as its own domain here and use its address as the sender.
Enabling custom SMTP sets Supabase's own limit to 30 emails per hour. That is enough for testing and too low for a launch. Raise it under Authentication → Rate Limits.
The same settings through the Management API — handy for several projects or environments:
# Access token: https://supabase.com/dashboard/account/tokens
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 '{
"smtp_admin_email": "noreply@your-domain.com",
"smtp_sender_name": "Acme",
"smtp_host": "smtp.postwing.app",
"smtp_port": 587,
"smtp_user": "token-login@your-domain.com",
"smtp_pass": "your-token-password"
}' For the local stack, in supabase/config.toml; supabase config push applies the file to a linked project:
# supabase/config.toml
[auth.email.smtp]
enabled = true
host = "smtp.postwing.app"
port = 587
user = "token-login@your-domain.com"
pass = "env(SMTP_TOKEN_PASSWORD)"
admin_email = "noreply@your-domain.com"
sender_name = "Acme"
[auth.rate_limit]
# Emails per hour
email_sent = 100 On self-hosted Supabase, the Docker setup reads the same values from its .env file:
# .env next to docker-compose.yml (self-hosted Supabase)
SMTP_ADMIN_EMAIL=noreply@your-domain.com
SMTP_HOST=smtp.postwing.app
SMTP_PORT=587
SMTP_USER=token-login@your-domain.com
SMTP_PASS=your-token-password
SMTP_SENDER_NAME=Acme Custom SMTP covers Supabase Auth only. For your own messages — receipts, notifications — Edge Functions are the natural place, but Supabase blocks outgoing connections to ports 25 and 587 from them. Call the REST API over HTTPS instead:
supabase secrets set MAIL_TOKEN_LOGIN=token-login@your-domain.com MAIL_TOKEN_PASSWORD=your-token-password// supabase/functions/order-confirmation/index.ts
// Call it from your backend or a database webhook — never let the browser pick the recipient.
Deno.serve(async (req) => {
const { email, orderId } = await req.json();
const res = await fetch("https://api.postwing.app/external/send_email_simple/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
to: [email],
subject: "Order #" + orderId + " confirmed",
body: "<p>Thanks — your order is confirmed.</p>",
sender: "Acme <noreply@your-domain.com>",
idempotency_key: "order-" + orderId,
auth: {
username: Deno.env.get("MAIL_TOKEN_LOGIN"),
password: Deno.env.get("MAIL_TOKEN_PASSWORD"),
},
}),
});
return new Response(await res.text(), { status: res.status });
});Sign up with an address outside your Supabase team, or send a password reset from Authentication → Users. If it arrives, custom SMTP is live — the built-in service would have refused that recipient. Failures show up in the project's Auth logs with the SMTP error.
| Error | Cause and fix |
|---|---|
Error sending confirmation email | Supabase could not hand the message over. Check the Auth logs: usually wrong credentials or the wrong port. |
email rate limit exceeded | Supabase's own hourly limit. Raise it under Authentication → Rate Limits. |
| Only team members receive email | Custom SMTP is not enabled or was not saved. |
| Links are expired on first click | A mail scanner opened the link first. Send the one-time code instead. |
| Edge Function hangs on SMTP | Port 587 is blocked there. Use the REST API or port 465. |
| Mail goes to spam | Sender email is not on your verified domain. |
That is the built-in email service. Until you configure custom SMTP, Supabase Auth refuses to deliver to addresses outside the project's team, limits sending to a couple of messages an hour and offers it best-effort only, for testing. Enabling custom SMTP lifts the recipient restriction.
Once custom SMTP is on, Supabase starts you at 30 emails per hour — a limit of its own, separate from your SMTP provider. Raise it under Authentication → Rate Limits once you know your sign-up volume. Hitting it returns an 'email rate limit exceeded' error to your users.
587, which Supabase's own examples use; the connection is upgraded with STARTTLS. Port 465 with implicit TLS works too. If you are self-hosting behind a network that blocks both, use 8587.
Yes. The [auth.email.smtp] section of supabase/config.toml configures the local stack, and supabase config push applies the same file to a linked project. Keep the password out of the file with env() substitution.
Not from Edge Functions on port 587: Supabase blocks outgoing connections to ports 25 and 587 there. Call the REST API with fetch instead, as shown above — or use port 465 if you need SMTP.
Corporate mail scanners such as Microsoft Defender open every link in a message on delivery, and that visit consumes the one-time token. Supabase recommends sending the 6-digit code ({{ .Token }}) instead, or linking to a page of your own that the user confirms from.