code examples

Sent logo
Sent TeamMar 8, 2026 / code examples / messagebird

Send SMS with MessageBird, Next.js, and Supabase

Build a Next.js application that sends SMS messages using MessageBird API and logs data to Supabase

Send SMS with MessageBird, Next.js, and Supabase

This comprehensive guide walks you through building a Next.js application that sends SMS messages using the MessageBird API and logs message data to Supabase. You'll create a production-ready API route with proper error handling, validation, and database integration—perfect for building SMS notifications, two-factor authentication, or transactional messaging systems.

Why This Stack?

  • MessageBird: Enterprise-grade SMS API with global coverage and competitive pricing
  • Next.js: React framework with built-in API routes, perfect for full-stack applications without separate backend infrastructure
  • Supabase: Open-source Firebase alternative providing PostgreSQL database with real-time capabilities and generous free tier

By the end of this tutorial, you'll have a functional Next.js API endpoint at /api/send-sms that accepts SMS requests, sends them via MessageBird, and logs all transactions to a Supabase database for tracking and analytics.

Key Technologies:

System Architecture:

[Client (Browser/cURL)] | | HTTP POST Request (/api/send-sms) v [Next.js API Route] | |-- Validates input |-- Initializes MessageBird SDK v [MessageBird API] ----> [Sends SMS to recipient] | |-- On success v [Supabase Client] ----> [Logs message to PostgreSQL database] | v [Returns response to client]

Prerequisites:

  • Node.js 18.17+ and npm: Download from nodejs.org
  • MessageBird Account: Sign up at MessageBird Dashboard
  • MessageBird API Key: Create a test or live key from the API Access (REST) tab in your MessageBird Dashboard under the Developers section
  • Supabase Account: Create a free account at supabase.com
  • Supabase Project: Initialize a new project in the Supabase Dashboard
  • Basic TypeScript/JavaScript Knowledge: Familiarity with async/await and REST APIs

1. Setting Up Your Next.js Project with MessageBird and Supabase

Next.js provides an optimized setup command that scaffolds a complete project structure.

  1. Create Next.js Project: Open your terminal and run the official Next.js creation command:

    bash
    npx create-next-app@latest messagebird-sms-app

    When prompted, select these options:

    • TypeScript: Yes (recommended for type safety)
    • ESLint: Yes
    • Tailwind CSS: Yes (optional, for styling)
    • src/ directory: No (keeps structure simpler)
    • App Router: No (we'll use Pages Router for API routes)
    • Import alias: Default (@/*)
  2. Navigate to Project Directory:

    bash
    cd messagebird-sms-app
  3. Install Required Dependencies: Add MessageBird SDK and Supabase client:

    bash
    npm install messagebird @supabase/supabase-js
  4. Create Environment Variables File: Next.js uses .env.local for environment variables that you should never commit:

    bash
    touch .env.local
  5. Configure .env.local: Add your API credentials (replace placeholders with actual values from your dashboards):

    env
    # MessageBird Configuration
    MESSAGEBIRD_API_KEY=YOUR_MESSAGEBIRD_ACCESS_KEY_HERE
    MESSAGEBIRD_ORIGINATOR=+14155550100
    
    # Supabase Configuration
    NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
    NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY_HERE

    Variable Explanations:

    • MESSAGEBIRD_API_KEY: Your MessageBird access key from the API Access page. Test keys have the test_ prefix.
    • MESSAGEBIRD_ORIGINATOR: The sender ID (phone number in E.164 format like +14155550100 or alphanumeric string up to 11 characters). Note: Alphanumeric senders aren't supported in all countries including the United States.
    • NEXT_PUBLIC_SUPABASE_URL: Your Supabase project URL from Project Settings > API
    • NEXT_PUBLIC_SUPABASE_ANON_KEY: Your Supabase anonymous public key (safe for client-side use with RLS policies)

    Security Note: The NEXT_PUBLIC_ prefix makes variables accessible in the browser. Only use this for public keys. Keep MESSAGEBIRD_API_KEY without the prefix to ensure it stays server-side only.

  6. Update .gitignore: Next.js automatically includes .env.local in .gitignore, but verify it contains:

    text
    # Environment Variables
    .env.local
    .env.*.local
    
    # Next.js Build Output
    .next/
    out/
    
    # Dependencies
    node_modules/

2. Setting Up Supabase Database

Before writing code, create a database table to store SMS message logs.

Creating the SMS Logs Table

  1. Navigate to SQL Editor: In your Supabase Dashboard, go to the SQL Editor section.

  2. Create Table with SQL: Run this SQL to create an sms_logs table with proper indexes:

    sql
    -- Create SMS logs table
    CREATE TABLE IF NOT EXISTS public.sms_logs (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      recipient TEXT NOT NULL,
      message TEXT NOT NULL,
      originator TEXT NOT NULL,
      message_id TEXT,
      status TEXT NOT NULL DEFAULT 'sent',
      created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
      updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
    );
    
    -- Add index for faster queries by recipient and date
    CREATE INDEX idx_sms_logs_recipient ON public.sms_logs(recipient);
    CREATE INDEX idx_sms_logs_created_at ON public.sms_logs(created_at DESC);
    
    -- Enable Row Level Security
    ALTER TABLE public.sms_logs ENABLE ROW LEVEL SECURITY;
    
    -- Create policy to allow service role to insert (for API route)
    CREATE POLICY "Enable insert for service role" ON public.sms_logs
      FOR INSERT
      WITH CHECK (true);
    
    -- Create policy to allow authenticated users to read their own logs
    CREATE POLICY "Enable read for all users" ON public.sms_logs
      FOR SELECT
      USING (true);

    Schema Explanation:

    • id: UUID primary key, automatically generated
    • recipient: Phone number that received the SMS (E.164 format)
    • message: Content of the SMS sent
    • originator: Sender ID used (your MessageBird number)
    • message_id: MessageBird's unique message identifier for tracking
    • status: Message status (sent, failed, etc.)
    • created_at/updated_at: Timestamps for record tracking
  3. Verify Table Creation: Go to the Table Editor in your Supabase Dashboard and confirm the sms_logs table exists with the correct columns.

Security Note: Row Level Security (RLS) is enabled to control access. The policies allow API routes (using service role key) to insert and all users to read. Adjust policies based on your security requirements.


3. Creating the Next.js API Route for SMS Sending

Next.js API Routes provide serverless functions for backend logic. Any file in pages/api/ becomes an API endpoint.

Create the API Route File

  1. Create API Directory Structure: If not already present, create the pages/api folder:

    bash
    mkdir -p pages/api
  2. Create send-sms.ts Route Handler: Create pages/api/send-sms.ts:

    typescript
    // pages/api/send-sms.ts
    import type { NextApiRequest, NextApiResponse } from 'next';
    import messagebird from 'messagebird';
    import { createClient } from '@supabase/supabase-js';
    
    // Type definitions for better type safety
    type ResponseData = {
      success: boolean;
      message: string;
      messageId?: string;
      error?: string;
    };
    
    type SMSRequestBody = {
      to: string;
      text: string;
    };
    
    // Initialize MessageBird client
    // Per MessageBird documentation: https://developers.messagebird.com/tutorials/send-sms-node
    const messagebirdClient = messagebird(process.env.MESSAGEBIRD_API_KEY as string);
    
    // Initialize Supabase client
    // Per Supabase documentation: https://supabase.com/docs/guides/getting-started/quickstarts/nextjs
    const supabase = createClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL as string,
      process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY as string
    );
    
    export default async function handler(
      req: NextApiRequest,
      res: NextApiResponse<ResponseData>
    ) {
      // Only accept POST requests
      if (req.method !== 'POST') {
        return res.status(405).json({
          success: false,
          message: 'Method not allowed. Use POST.',
        });
      }
    
      const { to, text } = req.body as SMSRequestBody;
    
      // Input validation
      if (!to || !text) {
        return res.status(400).json({
          success: false,
          message: 'Missing required fields: "to" (recipient) or "text" (message content).',
        });
      }
    
      // Validate E.164 phone number format
      // E.164 format: + followed by 1-15 digits
      const e164Regex = /^\+[1-9]\d{1,14}$/;
      if (!e164Regex.test(to)) {
        return res.status(400).json({
          success: false,
          message: 'Invalid phone number format. Use E.164 format (e.g., +14155550100).',
        });
      }
    
      // Validate message length (SMS limit is 160 characters per segment)
      if (text.length === 0) {
        return res.status(400).json({
          success: false,
          message: 'Message text cannot be empty.',
        });
      }
    
      try {
        // Send SMS via MessageBird
        // API Reference: https://developers.messagebird.com/api/
        const messagebirdResponse = await new Promise<any>((resolve, reject) => {
          messagebirdClient.messages.create(
            {
              originator: process.env.MESSAGEBIRD_ORIGINATOR as string,
              recipients: [to],
              body: text,
            },
            (err, response) => {
              if (err) {
                reject(err);
              } else {
                resolve(response);
              }
            }
          );
        });
    
        // Log to Supabase database
        const { data: logData, error: logError } = await supabase
          .from('sms_logs')
          .insert([
            {
              recipient: to,
              message: text,
              originator: process.env.MESSAGEBIRD_ORIGINATOR,
              message_id: messagebirdResponse.id,
              status: 'sent',
            },
          ])
          .select()
          .single();
    
        if (logError) {
          console.error('Supabase logging error:', logError);
          // Don't fail the request if logging fails, but log the error
        }
    
        return res.status(200).json({
          success: true,
          message: 'SMS sent successfully',
          messageId: messagebirdResponse.id,
        });
      } catch (error: any) {
        console.error('Error sending SMS:', error);
    
        // Log failed attempt to Supabase
        await supabase.from('sms_logs').insert([
          {
            recipient: to,
            message: text,
            originator: process.env.MESSAGEBIRD_ORIGINATOR,
            status: 'failed',
          },
        ]);
    
        // Handle MessageBird-specific errors
        // Error codes reference: https://developers.messagebird.com/api/
        const errorMessage =
          error.errors && error.errors[0]
            ? `MessageBird Error: ${error.errors[0].description}`
            : 'Failed to send SMS';
    
        return res.status(500).json({
          success: false,
          message: errorMessage,
          error: error.message,
        });
      }
    }

Code Walkthrough:

  • Client Initialization: Initialize the MessageBird client once at module level per SDK best practices. The Supabase client uses environment variables from .env.local.

  • Method Check: Next.js API routes handle all HTTP methods. We restrict this endpoint to POST only.

  • Input Validation: Validates required fields and E.164 phone format. E.164 is the international standard format required by MessageBird.

  • SMS Sending: Uses messages.create() method from MessageBird SDK with callback-style API wrapped in Promise for async/await compatibility.

  • Database Logging: Inserts success/failure record to Supabase. Non-blocking error handling ensures SMS delivery isn't blocked by logging failures.

  • Error Handling: Catches MessageBird API errors and returns structured error responses with proper HTTP status codes (400 for validation, 500 for server errors).


4. Testing Your SMS Integration

Start the Development Server

  1. Run Next.js Development Server:

    bash
    npm run dev

    Next.js displays output confirming the server is running:

    ready - started server on 0.0.0.0:3000, url: http://localhost:3000
  2. Verify Environment Variables: Check the terminal for any errors about missing environment variables. If you see errors, verify your .env.local file.

Test with cURL

  1. Send Test SMS: Open a new terminal and run:

    bash
    curl -X POST http://localhost:3000/api/send-sms \
      -H "Content-Type: application/json" \
      -d '{
        "to": "+1XXXXXXXXXX",
        "text": "Hello from MessageBird and Next.js!"
      }'

    Replace +1XXXXXXXXXX with a valid phone number in E.164 format. For MessageBird test accounts, verify the destination number first in your dashboard.

  2. Expected Success Response:

    json
    {
      "success": true,
      "message": "SMS sent successfully",
      "messageId": "abc123def456"
    }
  3. Validation Error Example (missing field):

    bash
    curl -X POST http://localhost:3000/api/send-sms \
      -H "Content-Type: application/json" \
      -d '{"to": "+14155550100"}'

    Response:

    json
    {
      "success": false,
      "message": "Missing required fields: \"to\" (recipient) or \"text\" (message content)."
    }
  4. Format Validation Error (invalid phone format):

    bash
    curl -X POST http://localhost:3000/api/send-sms \
      -H "Content-Type: application/json" \
      -d '{
        "to": "5551234",
        "text": "Test"
      }'

    Response:

    json
    {
      "success": false,
      "message": "Invalid phone number format. Use E.164 format (e.g., +14155550100)."
    }

Verify in Dashboards

  1. MessageBird Dashboard: Navigate to Developers > Message Logs to see sent messages with delivery status.

  2. Supabase Dashboard:

    • Go to Table Editor > sms_logs table
    • Verify entries appear with correct recipient, message, message_id, and status values
    • Check created_at timestamps match your test timing

5. Error Handling and Common Issues

MessageBird-Specific Error Codes

MessageBird returns structured error responses with error codes:

Error CodeDescriptionSolution
2Request not allowed (incorrect access_key)Verify MESSAGEBIRD_API_KEY in .env.local
9Missing paramsCheck that originator, recipients, and body are provided
10Invalid paramsVerify phone numbers are in E.164 format
20Not foundCheck the resource exists (e.g., valid originator number)
25Not enough balanceAdd credits to your MessageBird account
99Internal errorRetry request; contact MessageBird support if persists

Supabase Connection Errors

Common Supabase errors and solutions:

  • Invalid API Key: Verify NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are correct
  • Table Not Found: Ensure sms_logs table exists in public schema
  • Permission Denied: Check Row Level Security policies allow insertions
  • Network Timeout: Verify internet connection and Supabase project is active (not paused)

Next.js API Route Debugging

Environment Variables Not Loading:

  • Ensure .env.local is in project root directory (same level as package.json)
  • Restart dev server (npm run dev) after changing .env.local
  • Variables must not contain quotes: KEY=value not KEY="value"
  • Server-side variables (without NEXT_PUBLIC_) are only available in API routes

Module Not Found Errors:

bash
npm install messagebird @supabase/supabase-js

TypeScript Errors:

bash
npm install --save-dev @types/node

6. Security Best Practices for Production SMS Applications

Environment Variable Security

  • Never commit .env.local to version control. Next.js automatically ignores it in .gitignore.
  • Use server-side variables for sensitive keys (no NEXT_PUBLIC_ prefix for MESSAGEBIRD_API_KEY)
  • Rotate API keys regularly from MessageBird dashboard
  • Use test keys during development, live keys only in production

API Route Protection

For production, add authentication to prevent unauthorized SMS sending:

typescript
// pages/api/send-sms.ts
import { getSession } from 'next-auth/react'; // Example with NextAuth.js

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  // Check authentication
  const session = await getSession({ req });
  if (!session) {
    return res.status(401).json({
      success: false,
      message: 'Unauthorized. Authentication required.',
    });
  }

  // … rest of handler code
}

Rate Limiting

Implement rate limiting using Next.js middleware to prevent abuse:

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// Simple in-memory rate limiter (use Redis for production)
const rateLimitMap = new Map<string, number[]>();

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/send-sms')) {
    const ip = request.ip ?? 'unknown';
    const now = Date.now();
    const windowMs = 60 * 1000; // 1 minute
    const maxRequests = 10;

    const requests = rateLimitMap.get(ip) || [];
    const recentRequests = requests.filter((time) => now - time < windowMs);

    if (recentRequests.length >= maxRequests) {
      return NextResponse.json(
        { success: false, message: 'Too many requests. Try again later.' },
        { status: 429 }
      );
    }

    recentRequests.push(now);
    rateLimitMap.set(ip, recentRequests);
  }

  return NextResponse.next();
}

Production Rate Limiting: For distributed deployments (Vercel, AWS), use Upstash Rate Limiting with Redis or Vercel Edge Middleware rate limiting.

Supabase Row Level Security

The RLS policies created earlier protect your database. For production:

sql
-- Restrict reads to authenticated users only
DROP POLICY IF EXISTS "Enable read for all users" ON public.sms_logs;
CREATE POLICY "Enable read for authenticated users" ON public.sms_logs
  FOR SELECT
  USING (auth.role() = 'authenticated');

-- Restrict inserts to service role only (API routes)
-- Service role key should be used server-side only

Store the service role key separately for API routes:

env
# .env.local
SUPABASE_SERVICE_ROLE_KEY=YOUR_SERVICE_ROLE_KEY_HERE

7. Deployment Considerations

Deploying to Vercel

Vercel (creators of Next.js) provides optimal Next.js hosting:

  1. Install Vercel CLI:

    bash
    npm i -g vercel
  2. Deploy:

    bash
    vercel
  3. Configure Environment Variables:

    • Go to your Vercel Dashboard > Project > Settings > Environment Variables
    • Add all variables from .env.local:
      • MESSAGEBIRD_API_KEY
      • MESSAGEBIRD_ORIGINATOR
      • NEXT_PUBLIC_SUPABASE_URL
      • NEXT_PUBLIC_SUPABASE_ANON_KEY
    • Use live MessageBird API key (without test_ prefix) for production
  4. Redeploy: After adding environment variables, redeploy:

    bash
    vercel --prod

Serverless Function Considerations

Next.js API routes on Vercel run as serverless functions with limitations:

  • Execution Timeout: Free tier has 10 s timeout, Pro has 60 s
  • No Connection Pooling: Each request creates new database connections. Supabase client handles this automatically via REST API.
  • Cold Starts: First request after inactivity may be slower. Keep functions warm with uptime monitoring.

Alternative Deployment Platforms

Netlify:

bash
npm install -g netlify-cli
netlify deploy --prod

Add environment variables in Netlify Dashboard > Site settings > Environment variables.

AWS Amplify:

Follow Next.js deployment guide and configure environment variables in Amplify Console.

Self-Hosted (VPS/EC2):

bash
npm run build
npm start

Use process managers like PM2 and ensure environment variables are set in system environment or .env.production.local.


8. Next Steps and Enhancements

Receive SMS Messages (Webhooks)

Implement inbound SMS handling using MessageBird webhooks:

  1. Create Webhook Endpoint (pages/api/sms-webhook.ts):

    typescript
    import type { NextApiRequest, NextApiResponse } from 'next';
    
    export default async function handler(req: NextApiRequest, res: NextApiResponse) {
      if (req.method !== 'POST') {
        return res.status(405).json({ error: 'Method not allowed' });
      }
    
      const { originator, body, createdDatetime } = req.body;
    
      // Process inbound message
      console.log(`Received SMS from ${originator}: ${body}`);
    
      // Store in Supabase, trigger workflows, etc.
    
      return res.status(200).json({ success: true });
    }
  2. Configure in MessageBird: Go to Dashboard > Developers > Webhooks, set webhook URL to https://your-domain.com/api/sms-webhook

Delivery Status Tracking

Track SMS delivery status by implementing a webhook for delivery reports:

typescript
// pages/api/delivery-status.ts
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { id, status } = req.body; // MessageBird sends message ID and status

  await supabase
    .from('sms_logs')
    .update({ status })
    .eq('message_id', id);

  return res.status(200).json({ success: true });
}

Configure delivery report webhook URL in MessageBird Dashboard.

Build a Dashboard UI

Create a simple dashboard to view SMS logs:

typescript
// pages/sms-dashboard.tsx
import { useEffect, useState } from 'react';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

export default function SMSDashboard() {
  const [logs, setLogs] = useState([]);

  useEffect(() => {
    async function fetchLogs() {
      const { data } = await supabase
        .from('sms_logs')
        .select('*')
        .order('created_at', { ascending: false })
        .limit(50);
      setLogs(data || []);
    }
    fetchLogs();
  }, []);

  return (
    <div>
      <h1>SMS Logs</h1>
      <table>
        <thead>
          <tr>
            <th>Recipient</th>
            <th>Message</th>
            <th>Status</th>
            <th>Sent At</th>
          </tr>
        </thead>
        <tbody>
          {logs.map((log: any) => (
            <tr key={log.id}>
              <td>{log.recipient}</td>
              <td>{log.message}</td>
              <td>{log.status}</td>
              <td>{new Date(log.created_at).toLocaleString()}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

SMS Templates

Store reusable templates in Supabase:

sql
CREATE TABLE sms_templates (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  body TEXT NOT NULL,
  variables JSONB,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

Verification Checklist

  • Next.js project created with npx create-next-app
  • MessageBird and Supabase packages installed
  • .env.local configured with all required keys
  • Supabase sms_logs table created with RLS policies
  • API route created at pages/api/send-sms.ts
  • Development server runs without errors (npm run dev)
  • Test SMS sent successfully via cURL
  • SMS received on test phone number
  • Message logged in Supabase sms_logs table
  • Error handling tested (invalid phone format, missing fields)
  • MessageBird dashboard shows sent message
  • Production environment variables configured in deployment platform

Conclusion

You now have a production-ready Next.js application that sends SMS messages via MessageBird and logs transactions to Supabase. This foundation supports building notification systems, two-factor authentication, marketing campaigns, and customer communication platforms.

Key Resources:

For questions or issues, consult the official documentation or contact MessageBird Support or Supabase Support.