code examples
code examples
Send SMS with Plivo in Next.js and Supabase: Complete Guide
Build a secure SMS API endpoint using Plivo, Next.js 15 App Router, and Supabase authentication. Learn to send text messages programmatically with proper error handling and authentication.
Send SMS with Plivo in Next.js and Supabase: Complete Guide
This guide shows you how to build a secure Next.js API endpoint that sends SMS messages through Plivo, protected by Supabase authentication. You'll create a production-ready /api/send-sms route handler that accepts authenticated requests and delivers text messages programmatically.
By the end, you'll have a functional Next.js application with Supabase user authentication and a protected API route for sending SMS via Plivo.
Project Overview and Goals
Goal: Build a secure Next.js App Router API endpoint that authenticates users with Supabase, then sends SMS messages via the Plivo API.
Problem Solved: Provides authenticated SMS functionality for applications requiring secure, user-specific message delivery without exposing Plivo credentials to clients.
Technologies:
- Next.js 15: React framework with App Router for building full-stack applications
- Plivo Messages API: Cloud communication platform for sending SMS, voice, and messaging
- Plivo Node SDK (
plivo): Official Node.js library for Plivo API integration - Supabase: Backend-as-a-Service providing authentication, database, and real-time features
- Supabase Auth: User authentication and session management
System Architecture:
+----------------+ +------------------------+ +---------------+ +-----------+
| Next.js Client |------>| Next.js API Route |------>| Plivo API |------>| User Phone|
| (Authenticated)| | /api/send-sms | | (Messages) | | (SMS) |
| with Supabase | | Protected by Supabase | | | | |
+----------------+ +------------------------+ +---------------+ +-----------+
|
| Validates session with:
| - Supabase Auth Token
| - Server-side auth check
Prerequisites:
- Node.js 18+: Download from nodejs.org
- Plivo Account: Sign up at Plivo Console
- Plivo Auth ID and Auth Token: Find in your Plivo Dashboard
- Plivo Phone Number: Purchase a phone number with SMS capability
- Supabase Project: Create at supabase.com
- Supabase Project URL and Anon Key: Found in your Supabase project settings
- Basic understanding of Next.js App Router, React, and REST APIs
Final Outcome: A Next.js application with Supabase authentication where logged-in users can send SMS messages through a protected /api/send-sms endpoint powered by Plivo.
1. Setting Up the Project
Initialize your Next.js project with TypeScript and install required dependencies.
-
Create Next.js Application:
bashnpx create-next-app@latest plivo-sms-appWhen prompted, select:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes (optional, for styling)
src/directory: No- App Router: Yes
- Import alias: No (or default
@/*)
-
Navigate to Project:
bashcd plivo-sms-app -
Install Dependencies:
bashnpm install plivo @supabase/supabase-js @supabase/ssrplivo: Official Plivo Node.js SDK for sending SMS@supabase/supabase-js: Supabase JavaScript client@supabase/ssr: Supabase utilities for server-side rendering and API routes
-
Create Environment Variables File:
bashtouch .env.local -
Configure
.gitignore:Ensure
.env.localis listed in.gitignore(Next.js includes this by default):plaintext# .gitignore .env.local .env*.local -
Project Structure:
Your project structure should include:
plivo-sms-app/ ├── app/ │ ├── api/ │ │ └── send-sms/ │ │ └── route.ts # API endpoint for sending SMS │ ├── layout.tsx │ └── page.tsx ├── .env.local # Environment variables (gitignored) ├── .gitignore ├── package.json ├── tsconfig.json └── next.config.js
2. Configuring Environment Variables
Set up your Plivo and Supabase credentials securely using environment variables.
-
Open
.env.local:Add your Plivo and Supabase credentials:
bash# .env.local # Plivo API Credentials # Find these in your Plivo Console: https://console.plivo.com/dashboard/ PLIVO_AUTH_ID=your_plivo_auth_id PLIVO_AUTH_TOKEN=your_plivo_auth_token # Your Plivo phone number (sender ID) # Use E.164 format: +12015551234 PLIVO_FROM_NUMBER=+12015551234 # Supabase Configuration # Find these in your Supabase Project Settings -> API NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key -
Security Notes:
- Never commit
.env.localto version control - Variables prefixed with
NEXT_PUBLIC_are exposed to the browser – use only for public keys - Plivo credentials (Auth ID and Token) are server-side only (no
NEXT_PUBLIC_prefix) - Restart your Next.js dev server after changing environment variables
- Never commit
3. Setting Up Supabase Authentication
Create Supabase utility functions for server-side authentication in API routes.
-
Create Supabase Server Client Utility:
Create
lib/supabase/server.ts:typescript// lib/supabase/server.ts import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' export async function createClient() { const cookieStore = await cookies() return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { get(name: string) { return cookieStore.get(name)?.value }, }, } ) }This creates a Supabase client configured for Next.js App Router server components and route handlers, with automatic cookie management for session persistence.
-
Create Client-Side Supabase Utility:
Create
lib/supabase/client.tsfor client-side authentication:typescript// lib/supabase/client.ts import { createBrowserClient } from '@supabase/ssr' export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) }
4. Building the API Endpoint
Create the protected API route handler for sending SMS messages via Plivo.
-
Create API Route File:
Create
app/api/send-sms/route.ts:typescript// app/api/send-sms/route.ts import { NextRequest, NextResponse } from 'next/server' import plivo from 'plivo' import { createClient } from '@/lib/supabase/server' // Initialize Plivo client const plivoClient = new plivo.Client( process.env.PLIVO_AUTH_ID!, process.env.PLIVO_AUTH_TOKEN! ) export async function POST(request: NextRequest) { try { // 1. Authenticate user with Supabase const supabase = await createClient() const { data: { user }, error: authError } = await supabase.auth.getUser() if (authError || !user) { return NextResponse.json( { success: false, message: 'Unauthorized. Please log in.' }, { status: 401 } ) } // 2. Parse and validate request body const body = await request.json() const { to, text } = body const from = process.env.PLIVO_FROM_NUMBER if (!to || !text) { return NextResponse.json( { success: false, message: 'Missing required fields: "to" and "text" are required.' }, { status: 400 } ) } if (!from) { console.error('PLIVO_FROM_NUMBER not configured in environment variables') return NextResponse.json( { success: false, message: 'Server configuration error.' }, { status: 500 } ) } // 3. Validate input types if (typeof to !== 'string' || typeof text !== 'string') { return NextResponse.json( { success: false, message: 'Invalid input types. "to" and "text" must be strings.' }, { status: 400 } ) } // 4. Send SMS via Plivo console.log(`Sending SMS from ${from} to ${to} for user ${user.email}`) const response = await plivoClient.messages.create({ src: from, dst: to, text: text, }) console.log('Plivo API Response:', response) // 5. Return success response return NextResponse.json( { success: true, message: 'SMS sent successfully.', messageUuid: response.messageUuid, apiId: response.apiId, }, { status: 200 } ) } catch (error: any) { // Handle errors from Plivo or other sources console.error('Error sending SMS:', error) // Extract meaningful error information const errorMessage = error.message || 'An unexpected error occurred while sending SMS.' const statusCode = error.statusCode || 500 return NextResponse.json( { success: false, message: `Failed to send SMS: ${errorMessage}`, errorDetails: process.env.NODE_ENV === 'development' ? error : undefined }, { status: statusCode } ) } } -
Code Explanation:
- Authentication: Validates the user session using
supabase.auth.getUser(). Rejects unauthorized requests with 401 status. - Input Validation: Checks for required fields (
to,text) and validates types. - Plivo Integration: Uses
plivoClient.messages.create()withsrc(sender),dst(recipient), andtext(message content). - Error Handling: Catches exceptions from Plivo API calls and returns appropriate HTTP status codes with error details.
- Response Format: Returns JSON with
success,message, and Plivo response data (messageUuid,apiId).
- Authentication: Validates the user session using
-
Phone Number Format (E.164):
Use E.164 format for all phone numbers:
+[country code][number]- US:
+14155551234 - UK:
+447700900123 - India:
+919876543210 - Australia:
+61412345678
E.164 format ensures international compatibility and prevents routing errors.
- US:
5. Implementing Authentication UI
Create a simple authentication interface for users to log in with Supabase.
-
Create Login Page:
Create
app/login/page.tsx:typescript// app/login/page.tsx 'use client' import { useState } from 'react' import { createClient } from '@/lib/supabase/client' import { useRouter } from 'next/navigation' export default function LoginPage() { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [loading, setLoading] = useState(false) const [message, setMessage] = useState('') const router = useRouter() const supabase = createClient() const handleLogin = async (e: React.FormEvent) => { e.preventDefault() setLoading(true) setMessage('') const { error } = await supabase.auth.signInWithPassword({ email, password, }) setLoading(false) if (error) { setMessage(`Error: ${error.message}`) } else { setMessage('Login successful! Redirecting...') router.push('/') router.refresh() } } return ( <div style={{ maxWidth: '400px', margin: '100px auto', padding: '20px' }}> <h1>Login</h1> <form onSubmit={handleLogin}> <div style={{ marginBottom: '15px' }}> <label>Email:</label> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required style={{ width: '100%', padding: '8px', marginTop: '5px' }} /> </div> <div style={{ marginBottom: '15px' }}> <label>Password:</label> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required style={{ width: '100%', padding: '8px', marginTop: '5px' }} /> </div> <button type="submit" disabled={loading} style={{ padding: '10px 20px', cursor: 'pointer' }} > {loading ? 'Logging in...' : 'Log In'} </button> </form> {message && <p style={{ marginTop: '15px' }}>{message}</p>} </div> ) } -
Create SMS Sending Interface:
Update
app/page.tsxto create a form for authenticated users:typescript// app/page.tsx 'use client' import { useState, useEffect } from 'react' import { createClient } from '@/lib/supabase/client' import { useRouter } from 'next/navigation' export default function Home() { const [user, setUser] = useState<any>(null) const [to, setTo] = useState('') const [text, setText] = useState('') const [loading, setLoading] = useState(false) const [message, setMessage] = useState('') const router = useRouter() const supabase = createClient() useEffect(() => { const checkUser = async () => { const { data: { user } } = await supabase.auth.getUser() setUser(user) if (!user) { router.push('/login') } } checkUser() }, [router]) const handleSendSMS = async (e: React.FormEvent) => { e.preventDefault() setLoading(true) setMessage('') try { const response = await fetch('/api/send-sms', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ to, text }), }) const data = await response.json() if (data.success) { setMessage(`✓ SMS sent successfully! Message UUID: ${data.messageUuid}`) setTo('') setText('') } else { setMessage(`✗ Error: ${data.message}`) } } catch (error) { setMessage(`✗ Network error: ${error}`) } finally { setLoading(false) } } const handleLogout = async () => { await supabase.auth.signOut() router.push('/login') router.refresh() } if (!user) { return <div>Loading...</div> } return ( <div style={{ maxWidth: '600px', margin: '50px auto', padding: '20px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}> <h1>Send SMS with Plivo</h1> <button onClick={handleLogout} style={{ padding: '8px 16px' }}> Log Out </button> </div> <p>Logged in as: {user.email}</p> <form onSubmit={handleSendSMS} style={{ marginTop: '30px' }}> <div style={{ marginBottom: '15px' }}> <label>To (Phone Number in E.164 format):</label> <input type="tel" value={to} onChange={(e) => setTo(e.target.value)} placeholder="+14155551234" required style={{ width: '100%', padding: '8px', marginTop: '5px' }} /> </div> <div style={{ marginBottom: '15px' }}> <label>Message:</label> <textarea value={text} onChange={(e) => setText(e.target.value)} placeholder="Enter your message here" required rows={4} style={{ width: '100%', padding: '8px', marginTop: '5px' }} /> </div> <button type="submit" disabled={loading} style={{ padding: '10px 20px', cursor: 'pointer' }} > {loading ? 'Sending...' : 'Send SMS'} </button> </form> {message && ( <div style={{ marginTop: '20px', padding: '15px', backgroundColor: message.startsWith('✓') ? '#d4edda' : '#f8d7da', borderRadius: '4px' }}> {message} </div> )} </div> ) }
6. Testing the Application
Test your SMS functionality with Supabase authentication.
-
Create Test User in Supabase:
- Navigate to your Supabase project dashboard
- Go to Authentication → Users
- Click Add user → Create new user
- Enter email and password
- Confirm the user (or disable email confirmation in Authentication → Settings → Email Auth for development)
-
Start Development Server:
bashnpm run devYour application runs at
http://localhost:3000 -
Test Authentication Flow:
- Navigate to
http://localhost:3000 - You'll be redirected to
/login - Log in with your test user credentials
- Upon successful login, you'll return to the home page
- Navigate to
-
Test SMS Sending:
- Enter a recipient phone number in E.164 format (e.g.,
+14155551234) - Enter a message (e.g., "Test message from Plivo + Next.js")
- Click Send SMS
- Check the response message for success or errors
- Verify the SMS arrives at the recipient's phone
- Enter a recipient phone number in E.164 format (e.g.,
-
Test with cURL:
First, get an authentication token by logging in through the UI or using Supabase's API. Then test the endpoint:
bashcurl -X POST http://localhost:3000/api/send-sms \ -H 'Content-Type: application/json' \ -H 'Cookie: sb-access-token=YOUR_SESSION_TOKEN' \ -d '{ "to": "+14155551234", "text": "Test message via cURL" }' -
Expected Success Response:
json{ "success": true, "message": "SMS sent successfully.", "messageUuid": "abc123-def456-ghi789", "apiId": "xyz-789-456-123" } -
Expected Error Response (Unauthorized):
json{ "success": false, "message": "Unauthorized. Please log in." }
7. Adding Error Handling and Validation
Enhance your API route with comprehensive error handling and input validation.
-
Phone Number Validation:
Install a phone number validation library:
bashnpm install libphonenumber-jsUpdate
app/api/send-sms/route.tsto validate phone numbers:typescript// app/api/send-sms/route.ts (add import at top) import { parsePhoneNumber, isValidPhoneNumber } from 'libphonenumber-js' // ... existing code ... // Add validation before sending SMS (after type validation) // 4. Validate phone number format if (!isValidPhoneNumber(to)) { return NextResponse.json( { success: false, message: 'Invalid phone number format. Use E.164 format (+14155551234).' }, { status: 400 } ) } try { const phoneNumber = parsePhoneNumber(to) if (!phoneNumber.isValid()) { return NextResponse.json( { success: false, message: 'Phone number validation failed.' }, { status: 400 } ) } } catch (error) { return NextResponse.json( { success: false, message: 'Could not parse phone number.' }, { status: 400 } ) } // ... continue with sending SMS ... -
Message Length Validation:
Add length checks to prevent oversized messages:
typescript// Add after type validation // Validate message length (standard SMS is 160 characters for GSM-7, 70 for UCS-2) const MAX_SMS_LENGTH = 1600 // Allow up to 10 segments if (text.length === 0) { return NextResponse.json( { success: false, message: 'Message text cannot be empty.' }, { status: 400 } ) } if (text.length > MAX_SMS_LENGTH) { return NextResponse.json( { success: false, message: `Message too long. Maximum ${MAX_SMS_LENGTH} characters allowed. Current: ${text.length}` }, { status: 400 } ) } -
Rate Limiting:
Implement basic rate limiting to prevent abuse. Install
@upstash/ratelimitor use Next.js middleware:bashnpm install @upstash/ratelimit @upstash/redisCreate rate limiter utility in
lib/rate-limit.ts:typescript// lib/rate-limit.ts import { Ratelimit } from '@upstash/ratelimit' import { Redis } from '@upstash/redis' // Configure Upstash Redis in your .env.local: // UPSTASH_REDIS_REST_URL=... // UPSTASH_REDIS_REST_TOKEN=... export const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, '1 h'), // 10 requests per hour per user analytics: true, })Apply rate limiting in your API route:
typescript// app/api/send-sms/route.ts (add import) import { ratelimit } from '@/lib/rate-limit' // Add after authentication check // Rate limiting (after user authentication) const identifier = user.id // Use user ID as rate limit key const { success: rateLimitSuccess } = await ratelimit.limit(identifier) if (!rateLimitSuccess) { return NextResponse.json( { success: false, message: 'Rate limit exceeded. Please try again later.' }, { status: 429 } ) }
8. Security Best Practices
Implement security measures to protect your SMS endpoint and credentials.
-
Environment Variable Security:
- Never commit
.env.localto version control - Use different credentials for development, staging, and production
- Rotate Plivo Auth Token regularly
- Store production secrets in secure environment variable systems (Vercel, AWS Secrets Manager, etc.)
- Never commit
-
Input Sanitization:
While Plivo handles message encoding, sanitize inputs to prevent injection attacks:
typescript// Basic sanitization function function sanitizeInput(input: string): string { return input .trim() .replace(/[<>]/g, '') // Remove potential HTML/XML tags .substring(0, 1600) // Enforce max length } // Apply before sending const sanitizedText = sanitizeInput(text) -
CORS Configuration:
If your API will be called from external domains, configure CORS in
app/api/send-sms/route.ts:typescriptexport async function OPTIONS(request: NextRequest) { return new NextResponse(null, { status: 200, headers: { 'Access-Control-Allow-Origin': 'https://your-frontend-domain.com', 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, }) } -
Logging and Monitoring:
Implement structured logging for debugging and security monitoring:
typescript// Log security-relevant events console.log(JSON.stringify({ timestamp: new Date().toISOString(), event: 'sms_sent', userId: user.id, userEmail: user.email, destination: to.substring(0, 6) + '****', // Partially mask phone number messageLength: text.length, messageUuid: response.messageUuid, })) -
Message Content Filtering:
Implement content filtering to prevent abuse:
typescript// Basic spam/abuse detection const BLOCKED_KEYWORDS = ['spam', 'scam', 'phishing'] // Extend as needed function containsBlockedContent(text: string): boolean { const lowerText = text.toLowerCase() return BLOCKED_KEYWORDS.some(keyword => lowerText.includes(keyword)) } if (containsBlockedContent(text)) { console.warn(`Blocked message with suspicious content from user ${user.id}`) return NextResponse.json( { success: false, message: 'Message content not allowed.' }, { status: 400 } ) }
9. Deploying to Production
Deploy your Next.js application to Vercel or another hosting platform.
-
Prepare for Deployment:
- Ensure all environment variables are documented
- Test thoroughly in development
- Remove console.log statements or configure proper logging
- Set up Plivo webhook URLs for delivery receipts (if needed)
-
Deploy to Vercel:
bash# Install Vercel CLI npm i -g vercel # Deploy vercelFollow the prompts to link your project and deploy.
-
Configure Environment Variables in Vercel:
- Go to your Vercel project dashboard
- Navigate to Settings → Environment Variables
- Add all variables from
.env.local:PLIVO_AUTH_IDPLIVO_AUTH_TOKENPLIVO_FROM_NUMBERNEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYUPSTASH_REDIS_REST_URL(if using rate limiting)UPSTASH_REDIS_REST_TOKEN(if using rate limiting)
-
Configure Supabase for Production:
- Update Supabase project Authentication → URL Configuration
- Add your production domain to Site URL and Redirect URLs
- Enable email confirmation for production users
-
Test Production Deployment:
- Visit your production URL
- Test authentication flow
- Send test SMS
- Monitor Vercel logs for errors
- Check Plivo dashboard for message delivery status
-
Set Up Monitoring:
- Configure Vercel Analytics
- Set up error tracking (Sentry, LogRocket, etc.)
- Monitor Plivo usage and costs in Plivo Console
- Set up alerts for failed authentications or API errors
10. Handling Delivery Receipts and Webhooks
Configure Plivo webhooks to receive delivery status updates for sent messages.
-
Create Webhook Endpoint:
Create
app/api/webhooks/plivo/route.ts:typescript// app/api/webhooks/plivo/route.ts import { NextRequest, NextResponse } from 'next/server' export async function POST(request: NextRequest) { try { const body = await request.json() console.log('Plivo webhook received:', JSON.stringify(body, null, 2)) // Extract delivery status information const { MessageUUID, To, From, Status, TotalRate, TotalAmount, Units, ErrorCode } = body // Store delivery status in your database // await db.updateMessageStatus(MessageUUID, Status) console.log(`Message ${MessageUUID} status: ${Status}`) if (ErrorCode) { console.error(`Message ${MessageUUID} failed with error: ${ErrorCode}`) } return NextResponse.json({ success: true }, { status: 200 }) } catch (error) { console.error('Error processing Plivo webhook:', error) return NextResponse.json( { success: false, message: 'Webhook processing failed' }, { status: 500 } ) } } -
Configure Webhook in Plivo Console:
- Log into Plivo Console
- Navigate to Messaging → Applications (or use default application settings)
- Set Message URL to your webhook endpoint:
https://your-domain.com/api/webhooks/plivo - Set HTTP method to POST
- Save changes
-
Webhook Payload Example:
Plivo sends delivery status updates with this structure:
json{ "MessageUUID": "abc123-def456-ghi789", "From": "+12015551234", "To": "+14155556789", "Status": "delivered", "TotalRate": "0.00650", "TotalAmount": "0.00650", "Units": 1, "MCC": "310", "MNC": "260", "ErrorCode": null } -
Delivery Status Values:
queued: Message accepted and queued for deliverysent: Message dispatched to carrierdelivered: Message delivered to recipientundelivered: Message failed to deliverrejected: Message rejected by carrierfailed: Delivery failed
-
Store Status in Database:
Extend your application to store message records and update their delivery status. Example Supabase table schema:
sqlcreate table sms_messages ( id uuid default gen_random_uuid() primary key, user_id uuid references auth.users not null, message_uuid text unique not null, to_number text not null, from_number text not null, message_text text not null, status text default 'queued', error_code text, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- Enable Row Level Security alter table sms_messages enable row level security; -- Users can only view their own messages create policy "Users can view own messages" on sms_messages for select using (auth.uid() = user_id);
11. Advanced Features
Extend your SMS application with additional functionality.
Message Templates
Create reusable message templates:
// lib/message-templates.ts
export const templates = {
otp: (code: string) => `Your verification code is: ${code}. Valid for 10 minutes.`,
welcome: (name: string) => `Welcome to our service, ${name}! We're excited to have you.`,
reminder: (event: string, time: string) => `Reminder: ${event} at ${time}.`,
}
// Usage in API route
import { templates } from '@/lib/message-templates'
const message = templates.otp('123456')Bulk SMS Sending
Send messages to multiple recipients:
// app/api/send-bulk-sms/route.ts
export async function POST(request: NextRequest) {
// ... authentication ...
const { recipients, text } = await request.json() // recipients: string[]
const results = await Promise.allSettled(
recipients.map(to =>
plivoClient.messages.create({
src: process.env.PLIVO_FROM_NUMBER!,
dst: to,
text: text,
})
)
)
const successful = results.filter(r => r.status === 'fulfilled').length
const failed = results.filter(r => r.status === 'rejected').length
return NextResponse.json({
success: true,
sent: successful,
failed: failed,
total: recipients.length
})
}Scheduled Messages
Implement scheduled SMS using a job queue or cron:
// lib/scheduler.ts
import { createClient } from '@supabase/supabase-js'
export async function scheduleMessage(
userId: string,
to: string,
text: string,
sendAt: Date
) {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // Use service role for server-side
)
const { data, error } = await supabase
.from('scheduled_messages')
.insert({
user_id: userId,
to_number: to,
message_text: text,
send_at: sendAt.toISOString(),
status: 'scheduled'
})
return { data, error }
}Message History and Analytics
Track sent messages and generate analytics:
// app/api/messages/history/route.ts
import { createClient } from '@/lib/supabase/server'
export async function GET(request: NextRequest) {
const supabase = await createClient()
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { data: messages, error } = await supabase
.from('sms_messages')
.select('*')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
.limit(50)
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json({ messages })
}12. Troubleshooting Common Issues
Resolve frequent problems when integrating Plivo with Next.js and Supabase.
Issue: "Unauthorized" Error
Cause: Supabase session not found or expired.
Solution:
- Verify user is logged in through Supabase
- Check that cookies are enabled in the browser
- Ensure
createClient()is called with correct environment variables - Verify Supabase project URL and anon key are correct
Issue: Plivo Authentication Failed
Cause: Invalid PLIVO_AUTH_ID or PLIVO_AUTH_TOKEN.
Solution:
- Verify credentials in Plivo Console
- Check for trailing spaces in
.env.local - Ensure environment variables are loaded (restart dev server)
- Confirm you're using Auth ID and Auth Token, not API Key
Issue: "Invalid Phone Number" Error
Cause: Phone number not in E.164 format.
Solution:
- Ensure numbers start with
+followed by country code - Remove spaces, dashes, or parentheses
- Use
libphonenumber-jsto parse and validate - Test with:
+14155551234(US),+447700900123(UK)
Issue: Messages Not Delivered
Cause: Multiple potential causes.
Solution:
- Check Plivo Console → Logs → Messages for delivery status
- Verify sender number is active and has SMS capability
- Confirm recipient number is valid and reachable
- Check for carrier blocks or spam filters
- Review Plivo account balance and credit
Issue: Rate Limit Errors
Cause: Exceeding Plivo API rate limits or custom rate limits.
Solution:
- Review Plivo account limits in Console
- Implement exponential backoff for retries
- Reduce request frequency
- Consider upgrading Plivo account for higher limits
Issue: Environment Variables Not Found
Cause: .env.local not loaded or incorrect variable names.
Solution:
- Restart Next.js development server after changing
.env.local - Verify variable names match exactly (case-sensitive)
- Check
.env.localis in project root directory - Use
process.env.VARIABLE_NAMEnotprocess.env["VARIABLE_NAME"]
13. Cost Optimization and Best Practices
Minimize Plivo costs and optimize SMS delivery.
Monitor Usage and Costs
- Check Plivo Console → Account → Usage regularly
- Set up usage alerts in Plivo Console
- Track cost per message and total monthly spend
- Review message delivery rates to identify issues
Reduce Message Costs
- Optimize message length: Keep under 160 GSM-7 characters (or 70 UCS-2) to avoid multi-part messages
- Use local numbers: Send from numbers matching recipient country codes
- Batch processing: Send bulk messages during off-peak hours if possible
- Validate numbers: Check number validity before sending to avoid wasted attempts
Message Segmentation
SMS messages are charged per segment:
- GSM-7 encoding: 160 characters per segment
- UCS-2 encoding: 70 characters per segment (used for Unicode, emojis)
- Multi-part messages: First segment reduced to 153/67 characters for concatenation header
Calculate segments before sending:
function calculateSegments(text: string): number {
const isGSM7 = /^[@£$¥èéùìòÇØøÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !"#¤%&'()*+,\-./0-9:;<=>?¡A-ZÄÖÑܧ¿a-zäöñüà\n\r]+$/.test(text)
if (isGSM7) {
return text.length <= 160 ? 1 : Math.ceil(text.length / 153)
} else {
return text.length <= 70 ? 1 : Math.ceil(text.length / 67)
}
}
// Warn user before sending
const segments = calculateSegments(text)
if (segments > 1) {
console.warn(`Message will be sent as ${segments} segments`)
}Implement Delivery Confirmation
Only retry failed messages, not delivered ones:
// Check delivery status before retry
const status = await checkMessageStatus(messageUuid)
if (status === 'delivered') {
console.log('Message already delivered, skipping retry')
return
}Frequently Asked Questions About Plivo, Next.js, and Supabase SMS Integration
How do I send SMS with Plivo in a Next.js application?
To send SMS with Plivo in Next.js, create an API route using Next.js App Router, install the plivo SDK, and use client.messages.create() with your Auth ID, Auth Token, sender number (src), recipient number (dst), and message text. Authenticate the endpoint with Supabase Auth for secure access.
What's the difference between Plivo and Twilio for Next.js SMS?
Both Plivo and Twilio offer SMS APIs for Next.js applications. Plivo typically offers more competitive international SMS pricing and includes features like voice, messaging, and phone number management. Twilio has broader market adoption but may cost more for high-volume international messaging. Integration patterns are similar – both use REST APIs and official Node.js SDKs.
How do I protect my SMS API endpoint with Supabase authentication?
Protect your Next.js API route by creating a Supabase server client with createServerClient from @supabase/ssr, calling supabase.auth.getUser() to validate the session, and returning a 401 Unauthorized response if no valid user exists. This ensures only authenticated users can send SMS through your endpoint.
Can I use Plivo with Next.js 14 and 15 App Router?
Yes. Plivo works seamlessly with both Next.js 14 and 15 App Router. Create API routes in app/api/send-sms/route.ts, use the POST export function, and integrate the Plivo Node SDK (plivo package). The App Router's server-side nature is ideal for secure API key handling.
What environment variables do I need for Plivo SMS in Next.js?
You need four environment variables: PLIVO_AUTH_ID (your Plivo account ID), PLIVO_AUTH_TOKEN (authentication token), PLIVO_FROM_NUMBER (your Plivo phone number in E.164 format), and optionally Supabase credentials (NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY) for authentication. Store these in .env.local.
How much does it cost to send SMS with Plivo?
Plivo SMS pricing varies by destination country. US SMS typically costs $0.0065–$0.0075 per message segment (160 characters). International rates range from $0.01–$0.15+ per segment depending on the country. Check Plivo Console → Pricing for specific rates. Each 160-character GSM-7 message counts as one segment.
Can I send SMS to international numbers with Plivo and Next.js?
Yes. Use E.164 phone number format with the correct country code (e.g., +447700900123 for UK, +919876543210 for India). Verify your Plivo account has international SMS enabled in Console → Account → Settings. Some countries have additional sender ID requirements or regulatory restrictions.
How do I validate phone numbers before sending SMS in Next.js?
Install libphonenumber-js (npm install libphonenumber-js), import isValidPhoneNumber and parsePhoneNumber, and validate the recipient number before calling Plivo's API. This prevents failed sends due to invalid formats and reduces costs from rejected messages.
What's the best way to handle Plivo webhook delivery receipts in Next.js?
Create a webhook API route at app/api/webhooks/plivo/route.ts, export a POST function to receive delivery status updates, parse the webhook payload (MessageUUID, Status, ErrorCode), and store the status in your Supabase database. Configure the webhook URL in Plivo Console → Messaging → Applications.
Can I use Supabase Row Level Security with SMS message logs?
Yes. Create an sms_messages table in Supabase with a user_id column referencing auth.users, enable Row Level Security, and create a policy that allows users to view only their own messages (auth.uid() = user_id). This provides automatic, database-level authorization for SMS logs.
How do I implement rate limiting for my Plivo SMS endpoint?
Install @upstash/ratelimit and @upstash/redis, create a rate limiter instance with your chosen limits (e.g., 10 requests per hour per user), and check the rate limit in your API route after authenticating the user. Return 429 Too Many Requests if the limit is exceeded.
What's the message length limit for SMS with Plivo?
Standard SMS supports 160 characters using GSM-7 encoding. Messages with Unicode characters (emojis, special characters) use UCS-2 encoding, reducing the limit to 70 characters per segment. Longer messages automatically split into multiple segments, with each segment counted separately for billing.
Summary
You've built a complete, production-ready SMS application using:
- Plivo API: Cloud messaging platform for sending SMS
- Next.js 15 App Router: Modern React framework with server-side API routes
- Supabase Authentication: User authentication and session management
- TypeScript: Type-safe development
- Security best practices: Rate limiting, input validation, error handling
Key Features Implemented:
✓ Secure user authentication with Supabase ✓ Protected API endpoint for sending SMS ✓ Phone number validation with E.164 format ✓ Comprehensive error handling ✓ Rate limiting to prevent abuse ✓ Delivery receipt webhooks ✓ Production deployment ready
Next Steps:
- Implement message templates for common use cases
- Add SMS analytics and reporting dashboard
- Build automated messaging workflows
- Integrate two-factor authentication (2FA) using SMS
- Explore Plivo's voice and WhatsApp APIs
Frequently Asked Questions
What is the importance of E.164 number format in Vonage SMS?
E.164 is an international standard phone number format that includes the country code, ensuring unambiguous identification of the recipient's number for successful delivery.
How to send SMS with Node.js and Express?
Use the Vonage Messages API with the Express.js framework and Node.js. Set up an Express server, install the Vonage Server SDK, configure API credentials, create a '/send-sms' endpoint, and handle the request to send SMS messages programmatically.
What is Vonage Messages API used for in Node.js?
The Vonage Messages API allows Node.js applications to send SMS messages, as well as other types of messages like MMS and WhatsApp messages. It simplifies the process of integrating communication features into your application.
Why use dotenv with Vonage and Node.js?
Dotenv helps manage environment variables securely in your Node.js projects, including sensitive Vonage API credentials. This avoids hardcoding keys in your code, which is essential for security best practices.
When should I use ngrok with Vonage Messages API?
Ngrok is useful during development when testing Vonage's status webhooks locally, as it exposes your local server to the internet so Vonage can send webhook notifications.
Can I send SMS to international numbers using Vonage?
Yes, you can send SMS to international numbers with Vonage, but ensure your account is enabled for international messaging and you are following country-specific regulations. Use E.164 number formatting.
What is the Vonage Application ID and where to find it?
The Vonage Application ID is a unique identifier for your Vonage application, required to initialize the Vonage SDK. You can find it in your Vonage Dashboard under Applications -> Your Application.
How do I whitelist a number for testing SMS with Vonage?
If on a trial account, you'll need to add numbers under 'Numbers' -> 'Test numbers' to send test SMS messages to them from your Vonage virtual number
How to handle Vonage Messages API errors in Node.js?
Implement a try...catch block around the `vonage.messages.send()` call to handle potential errors during the API request. Return appropriate HTTP status codes and JSON error messages based on the error type and provide detailed logging for debugging.
What is the role of private key in Vonage API integration?
The private key is a crucial security credential for authenticating your Node.js application with the Vonage API. It must be kept secure and never committed to version control.
How to implement rate limiting for Vonage SMS endpoint?
Use middleware like 'express-rate-limit' to control the number of SMS requests from an IP address within a time window. Configure this middleware in your Express app to prevent abuse.
How to set up Vonage API credentials in Node.js?
Store Vonage API credentials (Application ID, Private Key Path, and virtual number) as environment variables in a '.env' file. Load these variables into your Node.js application using the 'dotenv' module. Never expose these credentials in code or version control.
How to receive SMS delivery reports with Vonage Messages API?
Configure the 'Status URL' in your Vonage Application settings. This webhook URL will receive delivery status updates. Create an endpoint to handle these webhooks.