Skip to content

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

  1. Your app requests a code with POST …/auth/v1/otp and { phone } (supabase-js signInWithOtp({ 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).
  2. AnvilBase generates a 6-digit code, stores it, and sends it to the user via the project’s SMS provider (Twilio).

  3. Your app collects the code and exchanges it for a session with POST …/auth/v1/verify using type: "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

Terminal window
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:

Terminal window
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"
}'
FieldMeaning
providerthe SMS vendor — twilio
account_sidTwilio Account SID (also the API Basic-auth username)
auth_tokenTwilio Auth Token — secret, stored encrypted, masked in responses
fromsender: a Twilio phone number (E.164) or a Messaging Service SID (MG…)

Reading the settings back masks the token:

Terminal window
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:

Terminal window
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.json
Authorization: Basic base64("<account_sid>:<auth_token>")
Content-Type: application/x-www-form-urlencoded
To=<phone>&From=<from>&Body=Your+verification+code+is+123456

The 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 OTPEmail OTPMagic link
ChannelSMSemailemail
What the user getsa 6-digit code to typea 6-digit code to typea link to click
RequestPOST …/auth/v1/otp with { phone }…/otp with { email }…/magiclink with { email }
CompletionPOST …/auth/v1/verify with { phone, token, type:'sms' }…/verify with { email, token, type:'email' }the emailed …/verify?type=magiclink&token=… URL
Per-project setupphone_enabled + SMS providerSMTPSMTP + 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.