Phone OTP with Supabase Edge Functions and StartMessaging
Add phone OTP login to a Supabase project using a Deno edge function that proxies the StartMessaging API. Includes function code, RLS rules, and a React client.
StartMessaging Team
Engineering
Supabase’s built-in phone auth assumes you have Twilio configured, which means DLT paperwork in India and a $1.50 minimum send. With a ~30-line edge function you can swap that for StartMessaging’s DLT-free OTP API and pay Rs 0.25 per send.
Why Not Supabase’s Built-in Phone Auth
Supabase’s phone auth provider list is global by default and every Indian-friendly option (Twilio, Vonage, MessageBird) requires DLT principal entity registration plus template approvals. That’s 2–6 weeks of paperwork before your first OTP. Wrapping StartMessaging in an edge function gets you live on the same day.
Edge Function: Send OTP
// supabase/functions/send-otp/index.ts
import { serve } from "https://deno.land/std@0.215.0/http/server.ts";
const KEY = Deno.env.get("STARTMESSAGING_API_KEY")!;
serve(async (req) => {
const { phone } = await req.json();
if (!/^\+91\d{10}$/.test(phone)) {
return new Response(JSON.stringify({ error: "invalid phone" }), { status: 400 });
}
const res = await fetch("https://api.startmessaging.com/otp/send", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": KEY },
body: JSON.stringify({ phoneNumber: phone, idempotencyKey: crypto.randomUUID() }),
});
const json = await res.json();
if (!res.ok) {
return new Response(JSON.stringify({ error: json.message }), { status: 502 });
}
return new Response(JSON.stringify({ requestId: json.data.requestId }), {
headers: { "Content-Type": "application/json" },
});
});Edge Function: Verify OTP
// supabase/functions/verify-otp/index.ts
import { serve } from "https://deno.land/std@0.215.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const KEY = Deno.env.get("STARTMESSAGING_API_KEY")!;
const SUPA = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
serve(async (req) => {
const { requestId, code, phone } = await req.json();
const res = await fetch("https://api.startmessaging.com/otp/verify", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": KEY },
body: JSON.stringify({ requestId, otpCode: code }),
});
const json = await res.json();
if (!res.ok || !json.data?.verified) {
return new Response(JSON.stringify({ verified: false }), { status: 401 });
}
// Upsert the user, mark phone verified
const { data: user, error } = await SUPA.auth.admin.createUser({
phone,
phone_confirm: true,
});
if (error && !error.message.includes("already")) {
return new Response(JSON.stringify({ error: error.message }), { status: 500 });
}
return new Response(JSON.stringify({ verified: true, user }));
});React Client
const { data: send } = await supabase.functions.invoke("send-otp", { body: { phone } });
// later, after the user enters the code
const { data: verified } = await supabase.functions.invoke("verify-otp", {
body: { requestId: send.requestId, code, phone },
});RLS Rules After Verification
Once the user is created with phone_confirm: true, they receive a normal Supabase JWT and your existing RLS policies based on auth.uid() work unchanged. There’s no need to bend your policies for the OTP flow.
Best Practices
- Set
STARTMESSAGING_API_KEYas a function secret, not a project env var. - Rate-limit by IP at the edge using a counter table or upstash redis.
- Never return the OTP code in the function response body.
- Log only request IDs, never phone numbers in plain text.
FAQ
Want to compare with a Next.js implementation? See our Next.js App Router OTP guide.
Related Articles
Send and verify SMS OTPs from a Next.js 14/15 App Router app using server actions and the StartMessaging API. Includes a full login form, server actions, and middleware.
Step-by-step Node.js tutorial to send and verify OTP via SMS using the StartMessaging API. Includes fetch examples, error handling, and verification flow.
Why prepaid wallet models work for Indian developers, how to forecast OTP spend, and how INR billing compares to DLT registration costs for SMS OTP.
Ready to Send OTPs?
Integrate StartMessaging in 5 minutes. No DLT registration required.