How to Send OTP in Supabase Edge Functions (2026 Guide)
Step-by-step tutorial for sending and verifying SMS OTPs in Supabase Edge Functions using Deno, TypeScript, and the StartMessaging API in India.
When Siddharth, lead full-stack developer at a Bengaluru SaaS startup, built his application on Supabase, he wanted to implement custom SMS phone authentication for Indian users. While Supabase provides built-in Auth, integrating third-party SMS gateways natively in India can be challenging due to high per-message costs, mandatory DLT template approvals, and rigid vendor integrations. Seeking a flexible serverless solution, Siddharth chose to build a custom phone verification pipeline using Supabase Edge Functions written in TypeScript and Deno. By pairing Supabase’s serverless edge environment with the StartMessaging API, his team achieved sub-3-second OTP delivery at ₹0.25/OTP without managing DLT portal approvals or complex infrastructure.
This tutorial provides a complete, step-by-step guide for sending and verifying SMS OTPs using Supabase Edge Functions. We cover Supabase CLI environment setup, Deno TypeScript functions, CORS handling, client-side React/Next.js integration, and production error handling.
Prerequisites for Supabase Edge Functions
Before writing your Edge Function, ensure you have the following ready:
- Supabase CLI installed on your local machine (
brew install supabase/tap/supabaseornpm i -g supabase). - A Supabase Project initialized (
supabase init). - A StartMessaging Account — sign up for free to get API credentials.
- An API Key — from your StartMessaging Dashboard (starts with
sm_live_).
Step 1: Set Up Supabase Secret Environment Variables
Store your StartMessaging API key securely in your Supabase project secrets using the Supabase CLI:
# Set StartMessaging API Key in local Supabase environment
supabase secrets set STARTMESSAGING_API_KEY=sm_live_xxxxxxxxxxxxxxxxxxxx
STARTMESSAGING_BASE_URL=https://api.startmessaging.com
Step 2: Create the send-otp Supabase Edge Function
Generate a new Edge Function using the CLI:
supabase functions new send-otp
Replace the generated supabase/functions/send-otp/index.ts file with the following Deno TypeScript code:
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
const API_KEY = Deno.env.get('STARTMESSAGING_API_KEY');
const BASE_URL = Deno.env.get('STARTMESSAGING_BASE_URL') || 'https://api.startmessaging.com';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
serve(async (req: Request) => {
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
if (!API_KEY) {
throw new Error('STARTMESSAGING_API_KEY environment variable is missing.');
}
const { phoneNumber } = await req.json();
if (!phoneNumber) {
return new Response(
JSON.stringify({ error: 'phoneNumber parameter is required.' }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// Call StartMessaging OTP API (Pre-approved DLT-free route)
const response = await fetch(`${BASE_URL}/otp/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
},
body: JSON.stringify({
phoneNumber,
}),
});
const payload = await response.json();
if (!response.ok) {
return new Response(
JSON.stringify({ error: payload.message || 'Failed to send OTP.' }),
{ status: response.status, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
return new Response(
JSON.stringify({
success: true,
requestId: payload.data.requestId,
expiresAt: payload.data.expiresAt,
message: 'OTP sent successfully.',
}),
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
} catch (error: any) {
return new Response(
JSON.stringify({ error: error.message || 'Internal server error.' }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
});
Step 3: Create the verify-otp Supabase Edge Function
Generate the verification function:
supabase functions new verify-otp
Replace supabase/functions/verify-otp/index.ts with:
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
const API_KEY = Deno.env.get('STARTMESSAGING_API_KEY');
const BASE_URL = Deno.env.get('STARTMESSAGING_BASE_URL') || 'https://api.startmessaging.com';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
serve(async (req: Request) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
if (!API_KEY) {
throw new Error('STARTMESSAGING_API_KEY environment variable is missing.');
}
const { requestId, otpCode } = await req.json();
if (!requestId || !otpCode) {
return new Response(
JSON.stringify({ error: 'requestId and otpCode parameters are required.' }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
const response = await fetch(`${BASE_URL}/otp/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
},
body: JSON.stringify({
requestId,
otpCode,
}),
});
const payload = await response.json();
if (!response.ok || payload.data?.verified !== true) {
return new Response(
JSON.stringify({ success: false, verified: false, error: payload.message || 'Invalid OTP code.' }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
return new Response(
JSON.stringify({ success: true, verified: true, message: 'Authentication successful.' }),
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
} catch (error: any) {
return new Response(
JSON.stringify({ error: error.message || 'Internal server error.' }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
});
Step 4: Deploy Edge Functions & Test Client Integration
Deploy your functions to Supabase:
supabase functions deploy send-otp
supabase functions deploy verify-otp
Next.js / React Client-Side Call Example
Invoke your Supabase Edge Functions directly from your React or Next.js frontend using @supabase/supabase-js:
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// 1. Send OTP Request
async function handleSendOtp(phoneNumber: string) {
const { data, error } = await supabase.functions.invoke('send-otp', {
body: { phoneNumber },
});
if (error) {
console.error('Failed to send OTP:', error.message);
return null;
}
console.log('OTP Sent. Request ID:', data.requestId);
return data.requestId;
}
// 2. Verify OTP Request
async function handleVerifyOtp(requestId: string, otpCode: string) {
const { data, error } = await supabase.functions.invoke('verify-otp', {
body: { requestId, otpCode },
});
if (error || !data.verified) {
console.error('Verification failed:', error?.message || 'Invalid code');
return false;
}
console.log('Phone number verified successfully!');
return true;
}
Frequently Asked Questions
Q: Why use Supabase Edge Functions instead of client-side SMS API calls?
A: Calling an SMS API directly from a client browser or mobile app exposes your secret STARTMESSAGING_API_KEY to public inspection. Supabase Edge Functions act as a secure serverless backend layer, keeping your API key in environment variables while executing requests close to your users via Deno’s global edge network.
Q: Do Supabase Edge Functions require DLT registration in India?
A: No. By using StartMessaging’s DLT-free OTP API within your Supabase Edge Functions, your messages are routed through pre-approved enterprise DLT headers (STARTMESSAGING). You do not need to register a Principal Entity ID (PE-ID) or submit DLT templates.
Q: How fast do Supabase Edge Functions process OTP dispatches in India?
A: Supabase Edge Functions execute in Deno deploy edge regions (such as Mumbai ap-south-1). Paired with StartMessaging’s sub-3-second SMS delivery pipeline, total round-trip execution from button click to handset delivery takes under 3 seconds.
After deploying these Supabase Edge Functions, Siddharth launched phone verification for his SaaS platform. His Deno TypeScript functions processed OTP requests securely with zero cold-start latency, delivering verification codes in under 3 seconds at ₹0.25/OTP without managing DLT compliance paperwork.
Ready to build phone auth in Supabase? Sign up for StartMessaging to send DLT-free OTPs at ₹0.25/OTP, or explore our SMS API documentation to test your serverless integration in under 5 minutes.
StartMessaging Team
StartMessaging Team