new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Supabase · Auth · all subjects

authentication/methods

414 notes in this subject, read out of this brain and free to use. This is page 2 of 7.

Before User Created Hook - Allow by Email Domain Example (SQL)

This SQL example creates a signup_email_domains table with id (serial primary key), domain (text not null), type (signup_email_domain_type enum with 'allow' or 'deny'), reason (text default null), created_at (timestamptz default now()), and updated_at (timestamptz default now()). The hook_restrict_signup_by_email_domain function extracts the email domain from the user.email field, checks if it matches an 'allow' type entry (allowing signup if found), then checks for a 'deny' type entry (rejecting with http_code 403 if found), and allows by default if no match. The function must be granted execute permission to supabase_auth_admin and revoked from authenticated, anon, and public roles.

Before User Created Hook - User Not Yet in Database

Because the hook runs immediately before insertion into the database, the user will not be found in Postgres at the time the hook is called.

Before User Created Hook - Output Response Format

The hook must return a JSON response with an optional error field. To allow the signup, return an empty object {} with HTTP 200 status or HTTP 204 No Content with no body. To reject the signup, return a JSON object with an error field containing http_code (integer) and message (string), using a 4xx HTTP status code. The error message is propagated to the client that attempted signup.

Before User Created Hook - Input Payload Structure

The before-user-created hook receives a payload with two top-level fields: metadata (object) containing uuid (string, uuid format), time (string, date-time format), ip_address (string, ipv4 format), and name (string, enum value 'before-user-created'); and user (object) containing id (string, uuid format), aud (string), role (string), email (string, email format), phone (string), app_metadata (object with required fields provider and providers array), user_metadata (object), identities (array of objects), created_at (string, date-time format), updated_at (string, date-time format), and is_anonymous (boolean). All fields in both metadata and user objects are required.

Before User Created Hook - Purpose and Behavior

The before-user-created hook runs before a new user is created in Supabase Auth. It allows developers to inspect the incoming user object and optionally reject the request. If the hook returns an error object, the signup is denied and the user is not created. If the hook responds successfully with HTTP 200 or 204 with no error, the request proceeds as usual. Use this hook to enforce custom signup policies that Supabase Auth does not handle natively, such as blocking disposable email domains, restricting access by region or IP, or requiring users to belong to a specific email domain.

Before User Created Hook - Block by OAuth Provider Example (SQL)

This SQL example creates the hook_reject_discord_signups function that extracts the provider from user.app_metadata.provider. If the provider equals 'discord', it returns an error with message 'Signups with Discord are not allowed.' and http_code 403. Otherwise it returns an empty object to allow the signup. This blocks new account creation via OAuth flows while allowing sign-ins with existing accounts. The function must be granted execute permission to supabase_auth_admin and revoked from authenticated, anon, and public roles.

Password Verification Hook example: email notification on failed attempts

Example SQL implementation that sends email notification when users exceed a threshold of failed login attempts (default 5 attempts per day). Creates password_sign_in_attempts table to track all attempts. Uses Supabase Vault to securely store email provider API key and pg_net to send HTTP POST request to email provider. Includes error handling for failed email sends and continues with default behavior if email succeeds.

Password Verification Hook example: rate limiting failed attempts

Example SQL implementation that limits users to one incorrect password attempt every 10 seconds. Creates a password_failed_verification_attempts table tracking last_failed_at per user_id. The hook checks if the last failed attempt was less than 10 seconds ago and returns a 429 HTTP error if so, preventing rapid brute force attempts while logging legitimate failed attempts.

Password Verification Hook output fields

The Password Verification Hook returns three fields: decision (string, either 'reject' to deny the verification attempt and log user out of all active sessions, or 'continue' for default Supabase Auth behavior), message (string, the message to show the user if decision was 'reject'), and should_logout_user (boolean, whether to log out the user if a 'reject' decision is issued; has no effect when 'continue' decision is issued).

Password Verification Hook input fields

The Password Verification Hook receives two input fields: user_id (string, unique identifier for the user attempting to sign in, correlates to auth.users table) and valid (boolean, whether the password verification attempt was valid).

Password Verification Hook security warning: abuse risk

As the Password Verification Hook runs on unauthenticated requests, malicious users can abuse the hook by calling it multiple times. You must check if a password is valid prior to taking any additional action to ensure the user is legitimate. Where possible, send an email or notification instead of blocking the user.

Password Verification Hook overview

The Password Verification Hook allows you to increase security beyond default password implementation to fulfill security or compliance requirements. You can track password sign-in attempt status and take action via email or login restrictions.

Send Email Hook environment variables for Resend integration

To use Resend as an email provider with the Send Email Hook, set two environment variables: RESEND_API_KEY (your Resend API key) and SEND_EMAIL_HOOK_SECRET (generated in the Auth Hooks section of the Supabase dashboard, in format 'v1,whsec_<base64_secret>').

Send Email Hook database queue pattern with SQL

This example shows how to use a PostgreSQL job queue with Send Email Hook to manage email sending asynchronously. It creates a job_queue table to store email jobs, a send_email function that inserts jobs into the queue with scheduling and priority, a dequeue_and_run_jobs function to process pending jobs with retry logic (max_retries default is 2, retry delay is 1 minute), and uses pg_cron to run the dequeue function every minute.

Send Email Hook with internationalization example

This example demonstrates adding internationalization to email templates using the Send Email Hook. It supports English, Spanish, and French email subjects and HTML templates for different email_action_types (signup, recovery, invite, magiclink, email_change, reauthentication). The language is determined from user.user_metadata.i18n with English as the default.

Send Email Hook with Resend example

This example shows how to configure Resend as a custom email provider through the Send Email Hook. It uses Supabase Edge Functions and the Resend API to send emails. The function verifies the webhook using standardwebhooks, extracts user and email_data from the payload, and sends an email via Resend's API with the token in the email body.

Email change behavior with Secure Email Change disabled

When email_action_type is email_change and Secure Email Change is disabled, only one OTP is generated for the new email. A single email must be sent to the new email address using either token with token_hash or token_new with token_hash, depending on which fields are present in the payload.

Email change behavior with Secure Email Change enabled

When email_action_type is email_change and Secure Email Change is enabled, two OTPs are generated: one for the current email (user.email) and one for the new email (user.new_email). Two emails must be sent. The token hash field names are counterintuitively reversed: token_hash_new is used with the current email address and token, while token_hash is used with the new email address and token_new.

Email sending behavior based on provider and hook status

Email sending depends on Email Provider and Auth Hook status. When Email Provider is Enabled and Auth Hook is Enabled, the Auth Hook handles email sending and SMTP is not used. When Email Provider is Enabled and Auth Hook is Disabled, SMTP handles email sending (custom if configured, default otherwise). When Email Provider is Disabled regardless of Auth Hook status, email signups are disabled.

email_data fields in Send Email Hook payload

The email_data object contains the following fields: token (6-digit string pattern), token_hash (16-30 character string), redirect_to (URL string), email_action_type (enum: signup, invite, magiclink, recovery, email_change, email, reauthentication, password_changed_notification, email_changed_notification, phone_changed_notification, identity_linked_notification, identity_unlinked_notification, mfa_factor_enrolled_notification, mfa_factor_unenrolled_notification), site_url (URL string), token_new (16-30 character string), token_hash_new (16-30 character string), old_email (email string), old_phone (phone string), provider (enum: email), and factor_type (enum: totp).

Send Email Hook output requirement

The Send Email Hook requires no outputs. An empty response with a status code of 200 is taken as a successful response.

Send Email Hook input parameters

The Send Email Hook receives two input parameters: 'user' (type: User object, contains the user account taking the action) and 'email' (type: object, metadata specific to the email sending process).

Send Email Hook purpose and use cases

The Send Email Hook replaces Supabase's built-in email sending. It can be used to send emails using your own email provider, add internationalization or custom logic, and fall back to another provider if your primary one fails.

Available Supabase client SDKs

Supabase provides client SDKs for JavaScript, Flutter, Swift, Python, C#, and Kotlin.

getUserIdentities() and unlinkIdentity() Dart example

final List<UserIdentity> identities = await supabase.auth.getUserIdentities(); final UserIdentity googleIdentity = identities.singleWhere((identity) => identity.provider == 'google'); await supabase.auth.unlinkIdentity(googleIdentity);

SAML SSO users cannot be identity linking targets

Users that signed up with SAML SSO will not be considered as targets for identity linking (automatic or manual) for security reasons.

Unlinking identity requirements

A user must be logged in and have at least 2 linked identities in order to unlink an existing identity.

Link identity with native OAuth using ID token

For native mobile applications, an identity can be linked using an ID token obtained from a third-party OAuth provider. This allows using native OAuth flows like Google Sign-In or Sign in with Apple rather than web-based OAuth redirects. The linkIdentity() method accepts a provider, token (ID token), and access_token parameter for native flows.

linkIdentityWithIdToken Dart example with Google Sign-In

import 'package:google_sign_in/google_sign_in.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; final GoogleSignIn googleSignIn = GoogleSignIn( clientId: iosClientId, serverClientId: webClientId, ); final googleUser = await googleSignIn.signIn(); final googleAuth = await googleUser!.authentication; final response = await supabase.auth.linkIdentityWithIdToken( provider: OAuthProvider.google, idToken: googleAuth.idToken!, accessToken: googleAuth.accessToken!, );

linkIdentityWithIdToken JavaScript example

const idToken = 'ID_TOKEN_FROM_GOOGLE' const accessToken = 'ACCESS_TOKEN_FROM_GOOGLE' const { data, error } = await supabase.auth.linkIdentity({ provider: 'google', token: idToken, access_token: accessToken, })

get_user_identities() and unlink_identity() Python example

response = supabase.auth.get_user_identities() google_identity = next((identity for identity in response.identities if identity.provider == 'google'), None) if google_identity: response = supabase.auth.unlink_identity(google_identity.identity_id)

UnlinkIdentity() C# example

var identities = supabase.Auth.CurrentUser.Identities; var googleIdentity = identities.First(x => x.Provider == "google"); await supabase.Auth.UnlinkIdentity(googleIdentity);

linkIdentity() Python example

response = supabase.auth.link_identity({'provider': 'google'})

linkIdentity() Kotlin example

supabase.auth.linkIdentity(Google)

linkIdentity() Swift example

try await supabase.auth.linkIdentity(provider: .google)

linkIdentity() JavaScript example

const { data, error } = await supabase.auth.linkIdentity({ provider: 'google' })

Manual identity linking with linkIdentity()

Supabase Auth allows a logged-in user to manually link an OAuth identity to their account with a different email address by calling the linkIdentity() method. The user is redirected to the OAuth provider to complete the OAuth 2.0 flow, and upon successful completion, the identity is linked to the user. Manual linking must be enabled from the project's authentication configuration options or by setting the environment variable GOTRUE_SECURITY_MANUAL_LINKING_ENABLED: true when self-hosting. Manual linking is currently in beta.

Automatic identity linking with same email

Supabase Auth automatically links identities with the same email address to a single user. When a new user signs in with OAuth, Supabase Auth attempts to look for an existing user using the same email address. If a match is found, the new identity is linked to the user. This improves user experience when multiple OAuth login options are presented since users do not need to remember which OAuth account they used to sign up with. For automatic linking to work correctly, all user emails must be unique and verified. When a new identity can be linked to an existing user, Supabase Auth removes any other unconfirmed identities linked to that existing user to prevent pre-account takeover attacks.

Add email/password to OAuth account

To add email with password authentication to an account created with an OAuth provider (Google, GitHub, etc.), call updateUser({ password: 'validpassword' }).

linkIdentityWithIdToken supported providers

The linkIdentityWithIdToken() method for Dart supports Google, Apple, Facebook, Kakao, and Keycloak OAuth providers.

Email signup after OAuth with same email prevents enumeration

If a user tries to create an email account after previously signing up with OAuth using the same email address, they will receive an obfuscated user response with no verification email sent. This prevents user enumeration attacks.

getUserIdentities() and unlinkIdentity() JavaScript example

const { data: identities, error: identitiesError } = await supabase.auth.getUserIdentities() if (!identitiesError) { const googleIdentity = identities.identities.find((identity) => identity.provider === 'google') if (googleIdentity) { const { data, error } = await supabase.auth.unlinkIdentity(googleIdentity) } }

LinkIdentity() C# example

var state = await supabase.Auth.LinkIdentity(Provider.Google, new SignInOptions { FlowType = OAuthFlowType.PKCE }); var authorizeUrl = state.Uri;

currentIdentitiesOrNull() and unlinkIdentity() Kotlin example

val identities = supabase.auth.currentIdentitiesOrNull() ?: emptyList() val googleIdentity = identities.first { it.provider == "google" } supabase.auth.unlinkIdentity(googleIdentity.identityId!!)

userIdentities() and unlinkIdentity() Swift example

let identities = try await supabase.auth.userIdentities() let googleIdentity = identities.first { $0.provider == .google } try await supabase.auth.unlinkIdentity(googleIdentity)

linkIdentity() Dart example

await supabase.auth.linkIdentity(OAuthProvider.google);

Send SMS Hook with WhatsApp and SMS fallback example

Example implementation that routes messages to WhatsApp for Latin American phone numbers and SMS for all others. Uses Twilio API with environment variables TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_WHATSAPP_NUMBER, and TWILIO_SMS_NUMBER. Detects country code from user.phone and checks against list of Latin American country codes to determine channel. WhatsApp messages use 'whatsapp:' prefix in To and From fields. ```javascript import { Webhook } from "https://esm.sh/standardwebhooks@1.0.0"; import { readAll } from "https://deno.land/std/io/read_all.ts"; import * as base64 from "https://denopkg.com/chiefbiiko/base64/mod.ts"; const accountSid: string | undefined = Deno.env.get("TWILIO_ACCOUNT_SID"); const authToken: string | undefined = Deno.env.get("TWILIO_AUTH_TOKEN"); const fromNumber: string = Deno.env.get("TWILIO_WHATSAPP_NUMBER"); const smsFromNumber: string = Deno.env.get("TWILIO_SMS_NUMBER"); const latinAmericanCountryCodes = ['54', '55', '56', '57', '58', '501', '502', '503', '504', '505', '506', '507', '508', '509', '51', '52', '53', '591', '592', '593', '594', '595', '596', '597', '598', '599']; const sendMessage = async ( messageBody: string, accountSid: string | undefined, authToken: string | undefined, fromNumber: string, toNumber: string, useWhatsApp: boolean, ): Promise < any > => { if (!accountSid || !authToken) { console.log("Your Twilio account credentials are missing. Please add them."); return; } const url: string = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`; const encodedCredentials: string = base64.fromUint8Array( new TextEncoder().encode(`${accountSid}:${authToken}`), ); const body: URLSearchParams = new URLSearchParams({ To: useWhatsApp ? `whatsapp:${toNumber}` : toNumber, From: useWhatsApp ? `whatsapp:${fromNumber}` : smsFromNumber, Body: messageBody, }); const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "Authorization": `Basic ${encodedCredentials}`, }, body, }); return response.json(); }; Deno.serve(async (req) => { const payload = await req.text(); const base64_secret = Deno.env.get("SEND_SMS_HOOK_SECRET").replace('v1,whsec_', ''); const headers = Object.fromEntries(req.headers); const wh = new Webhook(base64_secret); try { const { user, sms } = wh.verify(payload, headers); const messageBody = `Your OTP is: ${sms.otp}`; const userPhoneNumber = user.phone; const countryCode = userPhoneNumber.substring(1, userPhoneNumber.indexOf(userPhoneNumber.match(/\d/)!)); const useWhatsApp = latinAmericanCountryCodes.includes(countryCode); const response = await sendMessage( messageBody, accountSid, authToken, fromNumber, userPhoneNumber, useWhatsApp, ); if (response.status !== "queued") { return new Response( JSON.stringify({ error: `Failed to send message, Error Code: ${response.code} ${response.message} ${response.more_info}`, }), { status: response.status, headers: { "Content-Type": "application/json", }, }, ); } return new Response( JSON.stringify({ message: "Message sent successfully." }), { headers: { "Content-Type": "application/json", }, }, ); } catch (error) { return new Response( JSON.stringify({ error: `Failed to process the request: ${error}` }), { status: 500, headers: { "Content-Type": "application/json", }, }, ); } }); ```

Send SMS Hook with job queue pattern using SQL

Example implementation that queues SMS messages for asynchronous processing using a job_queue table and pg_cron scheduling. Creates job_queue table with fields: job_id (uuid primary key), job_data (jsonb), created_at (timestamp default now()), status (text default 'pending'), priority (int default 0), retry_count (int default 0), max_retries (int default 2), scheduled_at (timestamp default now()). The send_sms function extracts phone and otp from event, calculates nearest 5-minute window for scheduled_time, assigns priority based on time until scheduled execution, and inserts job into queue. The dequeue_and_run_jobs function processes pending jobs ordered by priority and created_at, with retry logic that delays 1 minute between retries up to max_retries. pg_cron is configured to run every minute ('* * * * *') to execute dequeue_and_run_jobs. Permissions grant supabase_auth_admin access to job_queue table and dequeue_and_run_jobs function, and revoke access from authenticated and anon roles. ```sql create table job_queue ( job_id uuid primary key default gen_random_uuid(), job_data jsonb not null, created_at timestamp default now(), status text default 'pending', priority int default 0, retry_count int default 0, max_retries int default 2, scheduled_at timestamp default now() ); create or replace function send_sms(event jsonb) returns void as $$ declare job_data jsonb; scheduled_time timestamp; priority int; begin job_data := jsonb_build_object( 'phone', event->'user'->>'phone', 'otp', event->'sms'->>'otp' ); scheduled_time := date_trunc('minute', now()) + interval '5 minute' * floor(extract('epoch' from (now() - date_trunc('minute', now())) / 60) / 5); priority := extract('epoch' from (scheduled_time - now()))::int; insert into job_queue (job_data, priority, scheduled_at, max_retries) values (job_data, priority, scheduled_time, 2); end; $$ language plpgsql; grant all on table public.job_queue to supabase_auth_admin; revoke all on table public.job_queue from authenticated, anon; create or replace function dequeue_and_run_jobs() returns void as $$ declare job record; begin for job in select * from job_queue where status = 'pending' and scheduled_at <= now() order by priority desc, created_at for update skip locked loop begin update job_queue set status = 'completed' where job_id = job.job_id; exception when others then if job.retry_count < job.max_retries then update job_queue set retry_count = retry_count + 1, scheduled_at = now() + interval '1 minute' where job_id = job.job_id; else update job_queue set status = 'failed' where job_id = job.job_id; end if; end; end loop; end; $$ language plpgsql; grant execute on function public.dequeue_and_run_jobs to supabase_auth_admin; revoke execute on function public.dequeue_and_run_jobs from authenticated, anon; select cron.schedule('* * * * *', 'select dequeue_and_run_jobs();'); ```

Send SMS Hook input schema

The Send SMS Hook receives two required input fields: 'user' (type: object, the User object from Supabase Auth) and 'sms' (type: object, metadata specific to SMS sending). The sms object contains 'otp' field (type: string, pattern: ^[0-9]{6}$, a 6-digit one-time password).

Send SMS Hook response requirement

The Send SMS Hook requires no outputs. An empty response with HTTP status code 200 is taken as a successful response.

Send SMS Hook purpose and use cases

The Send SMS Hook replaces Supabase's built-in SMS sending. It can be used to use a regional SMS Provider, use alternate messaging channels such as WhatsApp, fall back to another provider if the primary one fails, and adjust the message body to include platform specific fields such as AppHash.

User object in Send SMS Hook includes phone-related fields

The user object passed to the Send SMS Hook contains the following phone-related fields: phone (string), phone_confirmed_at (ISO 8601 date-time), phone_change_sent_at (ISO 8601 date-time), confirmation_sent_at (ISO 8601 date-time), confirmed_at (ISO 8601 date-time), and app_metadata with provider set to 'phone' and providers array containing 'phone'.

Send SMS Hook with Twilio example

Example implementation using Twilio to send SMS messages via the Send SMS Hook. The code uses Twilio API endpoint https://api.twilio.com/2010-04-01/Accounts/{accountSid}/Messages.json with POST method, expects environment variables TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER. The webhook verifies requests using standardwebhooks library and constructs message body as 'Your OTP is: {sms.otp}'. Success is determined by response.status === 'queued'. ```javascript import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0' import { readAll } from 'https://deno.land/std/io/read_all.ts' import { Twilio } from 'https://cdn.skypack.dev/twilio' import * as base64 from 'https://denopkg.com/chiefbiiko/base64/mod.ts' const accountSid: string | undefined = Deno.env.get('TWILIO_ACCOUNT_SID') const authToken: string | undefined = Deno.env.get('TWILIO_AUTH_TOKEN') const fromNumber: string = Deno.env.get('TWILIO_PHONE_NUMBER') const sendTextMessage = async ( messageBody: string, accountSid: string | undefined, authToken: string | undefined, fromNumber: string, toNumber: string ): Promise<any> => { if (!accountSid || !authToken) { console.log('Your Twilio account credentials are missing. Please add them.') return } const url: string = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json` const encodedCredentials: string = base64.fromUint8Array( new TextEncoder().encode(`${accountSid}:${authToken}`) ) const body: URLSearchParams = new URLSearchParams({ To: `+${toNumber}`, From: fromNumber, Body: messageBody, }) const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${encodedCredentials}`, }, body, }) return response.json() } Deno.serve(async (req) => { const payload = await req.text() const base64_secret = Deno.env.get('SEND_SMS_HOOK_SECRET').replace('v1,whsec_', '') const headers = Object.fromEntries(req.headers) const wh = new Webhook(base64_secret) try { const { user, sms } = wh.verify(payload, headers) const messageBody = `Your OTP is: ${sms.otp}` const response = await sendTextMessage( messageBody, accountSid, authToken, fromNumber, user.phone ) if (response.status !== 'queued') { return new Response( JSON.stringify({ error: { http_code: response.code, message: `Failed to send SMS: ${response.message}. More info: ${response.more_info}`, }, }), { status: response.status, headers: { 'Content-Type': 'application/json', }, } ) } return new Response( JSON.stringify({}), { status: 200, headers: { 'Content-Type': 'application/json', }, } ) } catch (error) { return new Response( JSON.stringify({ error: { http_code: 500, message: `Failed to send sms: ${JSON.stringify(error)}`, } }), { status: 500, headers: { 'Content-Type': 'application/json', }, } ) } }) ```

Use cases requiring custom SMTP

Custom SMTP must be set up for production use with the following Supabase Auth configurations: email and password accounts, passwordless accounts using one-time passwords or magic links sent over email, email-based user invitations, and social login with email confirmation.

Abuse mitigation: CAPTCHA protection

Configuring CAPTCHA protection is the most effective way to control bots attempting to abuse your SMTP sending reputation by signing up fake accounts. Services providing invisible CAPTCHA challenges are recommended so real users won't be asked to solve puzzles most of the time.

Custom SMTP server configuration via Management API

Custom SMTP can be configured using the Supabase Management API by making a PATCH request to https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth with the following parameters: external_email_enabled (boolean), mailer_secure_email_change_enabled (boolean), mailer_autoconfirm (boolean), smtp_admin_email (string), smtp_host (string), smtp_port (number), smtp_user (string), smtp_pass (string), and smtp_sender_name (string). The request requires an Authorization header with a Bearer token from the Supabase dashboard account tokens page.

Initial rate limit for custom SMTP

When custom SMTP is first configured, Supabase Auth imposes an initial low rate limit of 30 messages per hour to protect the reputation of the newly set up service. This can be adjusted to an acceptable value for the use case via the Rate Limits configuration page in the dashboard.

Default SMTP server restrictions and limitations

Supabase provides a default SMTP server for all projects to allow exploration and setup of email templates. This server has three key restrictions: (1) it sends messages only to pre-authorized addresses (project team members), refusing delivery to other addresses with an 'Email address not authorized' error; (2) it has significant rate limits currently set to a value that can change without notice; (3) it provides no SLA guarantee on message delivery or uptime. The default SMTP service is intended only for non-production use cases such as exploring Supabase Auth, setting up email templates with team members, and building toy projects or demos.

SMTP services compatible with Supabase Auth

Supabase Auth works with any email sending service that supports the SMTP protocol. Services known to work include: Resend, AWS SES, Postmark, Twilio SendGrid, ZeptoMail, and Brevo. To set up custom SMTP, obtain the SMTP server host, port, user, password, and a default From address from the chosen service.

Email sending service best practices

To maintain SMTP sending reputation: (1) configure DKIM, DMARC and SPF for the sending domain with your email service; (2) set up a custom domain for Auth messages to reduce spam classification from other Supabase projects' bad reputation; (3) use separate services and domains for Auth emails (e.g., auth.example.com) versus marketing emails (e.g., marketing.example.com); (4) have another SMTP service on stand-by in case the primary service has issues; (5) prepare for large user surges by working with your email service to adjust rate limits.

Give your agent this brain