Tutorials

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.

StartMessaging Team Updated

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/supabase or npm 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.

S

StartMessaging Team

StartMessaging Team

Related posts