Sending OTP via SMS
Build a secure OTP flow from a Supabase edge function — generate, send, verify, and expire.
Why First — The Scenario
A user signs up for your app with their mobile number. You need to verify that the number is real and belongs to them — not a typo, not someone else's number. Without this verification, users accidentally enter wrong numbers, and real users never receive their important updates.
OTP (One-Time Password) is the standard solution. Send a 6-digit code to the phone. If the user can type it back correctly within 5 minutes, the number is verified and belongs to them.
Every Indian app that handles transactions uses OTP. Banks, UPI apps, fee payment platforms, e-commerce — all of them. It is the most common SMS use case you will build.
Two Ways to Do OTP in Supabase
Before writing any code, understand that Supabase offers two paths for OTP:
Supabase Phone Auth (Built-in)
Supabase handles OTP completely — sends the SMS, stores the token, verifies it. You configure your Twilio or MSG91 account in the Supabase dashboard. Zero custom code.
Custom OTP via Edge Function
You control the full flow — generate OTP, send via MSG91, store in your DB, verify yourself. More code, but full control over the message text, template, expiry, and retry logic.
Which should you use?
| Scenario | Recommended Approach |
|---|---|
| Simple phone login/signup | Supabase built-in phone auth |
| OTP for a specific action (e.g. confirm payment) | Custom edge function |
| Need custom message text (DLT template) | Custom edge function |
| Need OTP as part of a multi-step flow | Custom edge function |
For most Indian apps with DLT requirements, the custom edge function approach is the right choice — because your DLT-approved template has specific wording that Supabase's built-in flow cannot match.
The Complete OTP Flow
The Supabase Table for OTP Storage
Before the edge function, create this table in Supabase:
Never Store Plain Text OTPs
Storing otp_hash (a SHA-256 hash) means even if your database is compromised, the attacker cannot read the OTPs. This is the same principle as storing password hashes instead of plain passwords. An OTP stored in plain text is a security failure — full stop.
The Send OTP Edge Function
Create this file at supabase/functions/send-otp/index.ts:
The Verify OTP Edge Function
Create this at supabase/functions/verify-otp/index.ts:
Required Supabase Secrets
Set these via supabase secrets set or the Supabase dashboard (Project Settings → Edge Functions → Secrets):
Security Rules for OTP
| Rule | Why |
|---|---|
| OTPs expire in 5 minutes | Limits the window of attack if intercepted |
| Max 3 attempts before invalidation | Prevents brute force (10^6 combinations) |
| Max 3 sends per 10 minutes | Prevents SMS bombing (someone spamming a victim's number) |
| Hash stored, never plain text | DB compromise does not expose OTPs |
| Mark as used immediately on success | Prevents OTP reuse |
| Invalidate old OTPs on new request | No stale OTPs floating around |
Next Step
With OTP working, move to Send Transactional SMS — fee payment confirmations, payment receipts, and other structured messages triggered by user actions.