SMSTunnel for Supabase Auth
Phone sign-in codes from your own phone and number
@narcisbodea/smstunnel-otp Codes from a number people know
Supabase Auth creates the code as usual. Instead of its built-in SMS provider, it calls your Send SMS hook: a small Edge Function that hands the code to your SMSTunnel account. Your own Android phone then sends it from its own SIM, so people get the code from a number they know, and there's no per-message bill from an SMS provider.
Before you start
- 1. Install the SMSTunnel app from Google Play on your Android phone and sign in.
- 2. In your dashboard, open Apps → Add an app, pick Sign-in codes, name it (for example "Supabase") and click Create the key.
- 3. Copy the key right away: it's shown only once. You can stop or delete the app there anytime, and Supabase can't send texts anymore.
Get the steps with your key in them
It opens Add an app with Sign-in codes picked. Once the key is made, the dashboard shows every step and command with the key already filled in, ready to copy.
Keep the key in your platform's secrets, never in code that runs in the browser. Codes must be 4 to 10 digits: the default 6-digit codes are fine; codes with letters aren't accepted.
1. Add the Edge Function
The package gives you the whole endpoint, including the Standard Webhooks signature check:
import { createSupabaseSendSmsHook } from 'npm:@narcisbodea/smstunnel-otp@^1.0.0';
Deno.serve(
createSupabaseSendSmsHook({
apiKey: Deno.env.get('SMSTUNNEL_API_KEY')!,
hookSecret: Deno.env.get('SEND_SMS_HOOK_SECRETS')!, // v1,whsec_...
template: 'Your Acme code is {code}', // optional
}),
);
A ready copy, with a deno.json for the import, is in
examples/supabase/functions/send-sms.
2. Deploy it and turn on the hook
- 1. Deploy it without JWT checks (the signature protects it):
supabase functions deploy send-sms --no-verify-jwt - 2. In the Supabase Dashboard:
Authentication → Hooks → Add a new hook → Send SMS hook, type HTTPS,
URL
https://<project-ref>.supabase.co/functions/v1/send-sms, then Generate secret and Create. - 3. Store the secrets:
supabase secrets set SMSTUNNEL_API_KEY=... SEND_SMS_HOOK_SECRETS='v1,whsec_...' - 4. Authentication → Sign In / Providers → Phone: enable it and save. The SMS provider fields aren't used while the hook is on. Keep phone auto-confirm off, or sign-ups never ask for a code.
What the hook does
-
It verifies the signature with your secret:
webhook-id,webhook-timestampandwebhook-signature, HMAC-SHA256, constant-time comparison, 5-minute window. Several secrets separated by|are accepted while you rotate. -
It sends the code to
sms.phone(Supabase sets it to the new number when a user changes theirs, and to the MFA factor's number), or else touser.phone. It adds the+that Supabase leaves out. -
It answers
200 {}on success. Errors use Supabase's shape,{"error":{"http_code":…,"message":"…"}}. A rate limit or an unreachable server answers 429 or 503 withretry-after, which Supabase retries within its 5-second budget. The default timeout is 4 s.
Local development
Add this to supabase/config.toml:
[auth.hook.send_sms]
enabled = true
uri = "http://host.docker.internal:54321/functions/v1/send-sms"
secrets = "env(SEND_SMS_HOOK_SECRETS)"
[functions.send-sms]
verify_jwt = false Worth knowing
- Codes have their own allowance. They go through SMSTunnel's code endpoint,
POST /api/v1/sms/send-2fa, which counts them apart from your other texts. See Pricing. - What counts as a code: 4 to 10 digits standing on their own, like
482913, or two groups of 3 digits, like123-456. A text without one is refused. Details - One number per text, up to 320 characters. The package refuses a longer text before calling SMSTunnel.
- 30 sends a minute. Past that, SMSTunnel answers 429 and asks to retry a little later.
- Phone offline? The code waits and goes out as soon as the phone is back.
- Nothing in the package logs codes or keys.
Build it with an AI assistant
Copy this prompt into Claude, ChatGPT, Cursor or Copilot and replace the part in [brackets]. Don't paste your API key into the chat: the prompt doesn't need it.
Help me send the phone sign-in codes of my Supabase Auth project from my own phone, with SMSTunnel.
About SMSTunnel (https://smstunnel.io): my own apps and tools hand text messages (SMS) to my SMSTunnel account, and my own Android phone sends them from my own number, on my own SIM and plan. The phone runs the SMSTunnel app from Google Play (https://play.google.com/store/apps/details?id=io.smstunnel.app), signed in to my account.
What I want: [describe what should send a text, to whom, and when]
How it works:
- Use the npm package @narcisbodea/smstunnel-otp (no dependencies, runs on Deno). It provides the Supabase "Send SMS hook" endpoint, including the Standard Webhooks signature check.
- Edge Function supabase/functions/send-sms/index.ts:
import { createSupabaseSendSmsHook } from 'npm:@narcisbodea/smstunnel-otp@^1.0.0';
Deno.serve(
createSupabaseSendSmsHook({
apiKey: Deno.env.get('SMSTUNNEL_API_KEY')!,
hookSecret: Deno.env.get('SEND_SMS_HOOK_SECRETS')!, // v1,whsec_...
template: 'Your Acme code is {code}', // optional
}),
);
- Deploy it without JWT checks (the signature protects it): supabase functions deploy send-sms --no-verify-jwt
- Supabase Dashboard → Authentication → Hooks → Add a new hook → Send SMS hook → type HTTPS, URL https://<project-ref>.supabase.co/functions/v1/send-sms → Generate secret → Create.
- Store the secrets: supabase secrets set SMSTUNNEL_API_KEY=... SEND_SMS_HOOK_SECRETS='v1,whsec_...'
- Authentication → Sign In / Providers → Phone: enable it and save. Keep phone auto-confirm off, or sign-ups never ask for a code.
- The hook sends the code to sms.phone, or else to user.phone, and adds the + that Supabase leaves out. It answers 200 {} on success and errors in Supabase's shape; a rate limit or an unreachable server answers 429 or 503 with retry-after, which Supabase retries within its 5-second budget.
- Codes go through SMSTunnel's POST https://smstunnel.io/api/v1/sms/send-2fa: the text must contain the code as 4 to 10 digits (Supabase's default 6 digits are fine) and can be up to 320 characters. Codes have their own daily allowance, apart from other texts. If the phone is offline, the code waits and goes out when it is back.
- The API key comes from my SMSTunnel dashboard (https://dashboard.smstunnel.io): Apps → Add an app → Create the key. It is shown only once and can send texts and check whether they went out.
Rules for your answer:
- Keep the API key in an environment variable or the platform's secret store (for example SMSTUNNEL_API_KEY). Never put it in source code, in a repository, or in code that runs in a browser or a mobile app: call SMSTunnel from a server. I will not paste my key in this chat; use a placeholder.
- Validate phone numbers before sending (international format, like +14155550123) and skip or report the invalid ones.
- Handle errors: log or show what went wrong, retry only after 429, 5xx or network errors and only a few times, and never send the same text twice.
- Only send texts people expect from me: alerts to myself, reminders, sign-in codes, updates they asked for. No unsolicited texts. Supabase is a trademark of its owner. SMSTunnel is a third-party integration and is not affiliated with, endorsed by, or sponsored by Supabase.
Sign-in codes for other platforms
Another platform? sendCode() from the same package
sends any code: see its README.