Phone / SMS auth
Phone auth lets a user sign in by entering a one-time 6-digit code sent to their phone over SMS — no password, no email. It is the SMS variant of passwordless auth, alongside Email OTP and Magic link.
SMS delivery goes through a pluggable SMS provider configured per project.
Twilio is the reference provider. Phone auth is opt-in — it is off until
you both enable phone_enabled and configure an SMS provider for the project.
The data-plane auth API is a GoTrue-compatible facade mounted under
/auth/v1, so the two endpoints below live at
/v1/auth/<project_id>/auth/v1/<endpoint> — the exact paths supabase-js calls.
The flow
-
Your app requests a code with
POST …/auth/v1/otpand{ phone }(supabase-jssignInWithOtp({ phone })). The phone number should be in E.164 form, e.g.+15551234567.Terminal window curl -X POST "http://localhost:39001/v1/auth/<project_id>/auth/v1/otp" \-H "apikey: $ANON_KEY" -H "Content-Type: application/json" \-d '{"phone":"+15551234567"}'- If phone auth is disabled for the project, you get a clear
422 otp_disabled. - If no SMS provider is configured, you get a clear
422 sms_provider_not_configured(the send never silently no-ops).
- If phone auth is disabled for the project, you get a clear
-
AnvilBase generates a 6-digit code, stores it, and sends it to the user via the project’s SMS provider (Twilio).
-
Your app collects the code and exchanges it for a session with
POST …/auth/v1/verifyusingtype: "sms":Terminal window curl -X POST "http://localhost:39001/v1/auth/<project_id>/auth/v1/verify" \-H "apikey: $ANON_KEY" -H "Content-Type: application/json" \-d '{"phone":"+15551234567","token":"123456","type":"sms"}'A valid code returns a full project session (a JWT pair). The user is created on first successful verify (passwordless sign-up), and the returned user carries the
phone:{"user": { "id": "…", "phone": "+15551234567", "email": null, "...": "…" },"access_token": "<jwt>","token_type": "bearer","expires_in": 3600,"expires_at": 1781233600,"refresh_token": "<jwt>"}
SDK example
// Request the SMS code.await db.auth.signInWithOtp({ phone: "+15551234567" })
// …user reads the code from their phone, then exchange it for a session:const { data, error } = await db.auth.verifyOtp({ phone: "+15551234567", token: "123456", type: "sms",})// data.session is adopted automatically, so subsequent db.from()/db.storage()// calls run as the authenticated user.This is drop-in compatible with @supabase/supabase-js.
Enable phone auth (management plane)
Phone auth needs two things turned on: the phone_enabled toggle and a
configured SMS provider.
1. Toggle phone_enabled
curl -X PATCH http://localhost:39001/api/v1/projects/<id>/auth \ -H "Authorization: Bearer $ANVILBASE_TOKEN" -H "Content-Type: application/json" \ -d '{ "phone_enabled": true }'2. Configure the SMS provider (Twilio)
Set the project’s SMS provider. The auth_token is a secret — it is stored
encrypted at rest (AES-256-GCM, exactly like an OAuth client secret) and is
only ever returned masked:
curl -X PUT http://localhost:39001/api/v1/projects/<id>/auth/sms \ -H "Authorization: Bearer $ANVILBASE_TOKEN" -H "Content-Type: application/json" \ -d '{ "provider": "twilio", "account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "auth_token": "your-twilio-auth-token", "from": "+15550001111" }'| Field | Meaning |
|---|---|
provider | the SMS vendor — twilio |
account_sid | Twilio Account SID (also the API Basic-auth username) |
auth_token | Twilio Auth Token — secret, stored encrypted, masked in responses |
from | sender: a Twilio phone number (E.164) or a Messaging Service SID (MG…) |
Reading the settings back masks the token:
curl http://localhost:39001/api/v1/projects/<id>/auth \ -H "Authorization: Bearer $ANVILBASE_TOKEN"# → { …, "phone_enabled": true,# "sms": { "provider": "twilio", "account_sid": "AC…",# "auth_token_masked": "your…oken", "from": "+15550001111" } }Remove the SMS provider with:
curl -X DELETE http://localhost:39001/api/v1/projects/<id>/auth/sms \ -H "Authorization: Bearer $ANVILBASE_TOKEN"How Twilio is called
AnvilBase calls the Twilio REST API directly — there is no vendor SDK dependency and no control-plane SMS relay; the auth service sends the SMS itself using the project’s decrypted credentials. Each OTP is delivered with:
POST https://api.twilio.com/2010-04-01/Accounts/<account_sid>/Messages.jsonAuthorization: Basic base64("<account_sid>:<auth_token>")Content-Type: application/x-www-form-urlencoded
To=<phone>&From=<from>&Body=Your+verification+code+is+123456The provider abstraction is structured so additional vendors (e.g. Vonage, MessageBird) can be added later without changing this end-user contract.
OTP vs other passwordless methods
| Phone / SMS OTP | Email OTP | Magic link | |
|---|---|---|---|
| Channel | SMS | ||
| What the user gets | a 6-digit code to type | a 6-digit code to type | a link to click |
| Request | POST …/auth/v1/otp with { phone } | …/otp with { email } | …/magiclink with { email } |
| Completion | POST …/auth/v1/verify with { phone, token, type:'sms' } | …/verify with { email, token, type:'email' } | the emailed …/verify?type=magiclink&token=… URL |
| Per-project setup | phone_enabled + SMS provider | SMTP | SMTP + magic_link_enabled |
Rate limits
Because each SMS costs money, phone OTP sends are throttled harder than email: a
per-IP guard (5 sends/min per source IP) plus a per-recipient hourly cap
(OTP_SMS_SEND_MAX, default 3, over OTP_SEND_WINDOW_SECS). Over the cap
returns 429 over_sms_send_rate_limit with Retry-After, matching Supabase.
The cap is recipient-keyed (no account-existence leak) and blocks SMS bombing /
toll fraud. See Network Security and
Configuration.
Next: Multi-factor auth.