code examples
code examples
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:
- Node.js 18+: JavaScript runtime (Next.js requires Node.js 18.17 or later per Next.js documentation)
- Next.js 14+: React framework with API Routes for backend functionality
- MessageBird SMS API: RESTful API for sending SMS messages worldwide via the messagebird Node.js SDK
- Supabase: PostgreSQL database with JavaScript client library @supabase/supabase-js
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.
-
Create Next.js Project: Open your terminal and run the official Next.js creation command:
bashnpx create-next-app@latest messagebird-sms-appWhen 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 (@/*)
-
Navigate to Project Directory:
bashcd messagebird-sms-app -
Install Required Dependencies: Add MessageBird SDK and Supabase client:
bashnpm install messagebird @supabase/supabase-jsmessagebird: Official MessageBird Node.js SDK for SMS API integration@supabase/supabase-js: Supabase JavaScript client for database operations
-
Create Environment Variables File: Next.js uses
.env.localfor environment variables that you should never commit:bashtouch .env.local -
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_HEREVariable Explanations:
MESSAGEBIRD_API_KEY: Your MessageBird access key from the API Access page. Test keys have thetest_prefix.MESSAGEBIRD_ORIGINATOR: The sender ID (phone number in E.164 format like+14155550100or 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 > APINEXT_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. KeepMESSAGEBIRD_API_KEYwithout the prefix to ensure it stays server-side only. -
Update
.gitignore: Next.js automatically includes.env.localin.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
-
Navigate to SQL Editor: In your Supabase Dashboard, go to the SQL Editor section.
-
Create Table with SQL: Run this SQL to create an
sms_logstable 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 generatedrecipient: Phone number that received the SMS (E.164 format)message: Content of the SMS sentoriginator: Sender ID used (your MessageBird number)message_id: MessageBird's unique message identifier for trackingstatus: Message status (sent, failed, etc.)created_at/updated_at: Timestamps for record tracking
-
Verify Table Creation: Go to the Table Editor in your Supabase Dashboard and confirm the
sms_logstable 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
-
Create API Directory Structure: If not already present, create the pages/api folder:
bashmkdir -p pages/api -
Create
send-sms.tsRoute Handler: Createpages/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
-
Run Next.js Development Server:
bashnpm run devNext.js displays output confirming the server is running:
ready - started server on 0.0.0.0:3000, url: http://localhost:3000 -
Verify Environment Variables: Check the terminal for any errors about missing environment variables. If you see errors, verify your
.env.localfile.
Test with cURL
-
Send Test SMS: Open a new terminal and run:
bashcurl -X POST http://localhost:3000/api/send-sms \ -H "Content-Type: application/json" \ -d '{ "to": "+1XXXXXXXXXX", "text": "Hello from MessageBird and Next.js!" }'Replace
+1XXXXXXXXXXwith a valid phone number in E.164 format. For MessageBird test accounts, verify the destination number first in your dashboard. -
Expected Success Response:
json{ "success": true, "message": "SMS sent successfully", "messageId": "abc123def456" } -
Validation Error Example (missing field):
bashcurl -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)." } -
Format Validation Error (invalid phone format):
bashcurl -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
-
MessageBird Dashboard: Navigate to Developers > Message Logs to see sent messages with delivery status.
-
Supabase Dashboard:
- Go to Table Editor >
sms_logstable - Verify entries appear with correct
recipient,message,message_id, andstatusvalues - Check
created_attimestamps match your test timing
- Go to Table Editor >
5. Error Handling and Common Issues
MessageBird-Specific Error Codes
MessageBird returns structured error responses with error codes:
| Error Code | Description | Solution |
|---|---|---|
2 | Request not allowed (incorrect access_key) | Verify MESSAGEBIRD_API_KEY in .env.local |
9 | Missing params | Check that originator, recipients, and body are provided |
10 | Invalid params | Verify phone numbers are in E.164 format |
20 | Not found | Check the resource exists (e.g., valid originator number) |
25 | Not enough balance | Add credits to your MessageBird account |
99 | Internal error | Retry request; contact MessageBird support if persists |
Supabase Connection Errors
Common Supabase errors and solutions:
- Invalid API Key: Verify
NEXT_PUBLIC_SUPABASE_URLandNEXT_PUBLIC_SUPABASE_ANON_KEYare correct - Table Not Found: Ensure
sms_logstable 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.localis in project root directory (same level aspackage.json) - Restart dev server (
npm run dev) after changing.env.local - Variables must not contain quotes:
KEY=valuenotKEY="value" - Server-side variables (without
NEXT_PUBLIC_) are only available in API routes
Module Not Found Errors:
npm install messagebird @supabase/supabase-jsTypeScript Errors:
npm install --save-dev @types/node6. Security Best Practices for Production SMS Applications
Environment Variable Security
- Never commit
.env.localto version control. Next.js automatically ignores it in.gitignore. - Use server-side variables for sensitive keys (no
NEXT_PUBLIC_prefix forMESSAGEBIRD_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:
// 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:
// 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:
-- 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 onlyStore the service role key separately for API routes:
# .env.local
SUPABASE_SERVICE_ROLE_KEY=YOUR_SERVICE_ROLE_KEY_HERE7. Deployment Considerations
Deploying to Vercel
Vercel (creators of Next.js) provides optimal Next.js hosting:
-
Install Vercel CLI:
bashnpm i -g vercel -
Deploy:
bashvercel -
Configure Environment Variables:
- Go to your Vercel Dashboard > Project > Settings > Environment Variables
- Add all variables from
.env.local:MESSAGEBIRD_API_KEYMESSAGEBIRD_ORIGINATORNEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY
- Use live MessageBird API key (without
test_prefix) for production
-
Redeploy: After adding environment variables, redeploy:
bashvercel --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:
npm install -g netlify-cli
netlify deploy --prodAdd 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):
npm run build
npm startUse 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:
-
Create Webhook Endpoint (
pages/api/sms-webhook.ts):typescriptimport 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 }); } -
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:
// 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:
// 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:
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.localconfigured with all required keys - Supabase
sms_logstable 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_logstable - 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:
- MessageBird API Documentation
- MessageBird Node.js SDK Tutorial
- Next.js API Routes Documentation
- Supabase Next.js Quickstart
- Supabase Database Tables Guide
For questions or issues, consult the official documentation or contact MessageBird Support or Supabase Support.