code examples
code examples
Twilio WhatsApp Integration with Next.js and Supabase: Complete Guide
A step-by-step guide to building a Node.js/Express application for sending and receiving WhatsApp messages via the Vonage Messages API, including setup, webhooks, signature verification, and deployment considerations.
.env
This comprehensive guide demonstrates how to build a production-ready WhatsApp messaging application using Twilio's WhatsApp API, Next.js for serverless API routes, and Supabase for authentication and data persistence. You'll learn to send and receive WhatsApp messages programmatically, implement secure user authentication, store conversation history in a PostgreSQL database, and handle real-time webhook callbacks.
By completing this tutorial, you'll have a functional Next.js application that:
- Sends WhatsApp messages using the Twilio WhatsApp Sandbox or production number
- Receives and processes inbound messages through Twilio webhook integration
- Authenticates users with Supabase Auth for secure access control
- Stores message history in a Supabase PostgreSQL database with real-time capabilities
- Handles delivery status updates for message tracking and analytics
This solution addresses common business needs for integrating WhatsApp messaging into web applications for customer support, automated notifications, and conversational workflows.
Technologies and Stack
Core Technologies:
- Next.js 14+: React framework with App Router for building frontend UI and serverless API routes
- Supabase: Open-source Backend-as-a-Service providing PostgreSQL database, authentication, and real-time subscriptions
- Twilio WhatsApp API: Cloud communications platform for programmatic WhatsApp message sending and receiving
- Node.js 18+: JavaScript runtime environment (v18 LTS or v20 recommended for Next.js 14+ compatibility)
- TypeScript (Optional but Recommended): Type-safe development for production applications
Key Dependencies:
twilio- Official Twilio SDK for Node.js with WhatsApp API support@supabase/supabase-js- Supabase client library for database operations and authentication@supabase/ssr- Server-side rendering helpers for Next.js with Supabase@supabase/auth-helpers-nextjs- Authentication utilities for Next.js middleware and API routesngrok- Local development tunnel for testing webhooks during development
Prerequisites
Required Accounts and Tools:
- Twilio Account: Sign up for free - includes trial credits and WhatsApp Sandbox access
- Supabase Account: Create a free project - includes 500MB PostgreSQL database and 50,000 monthly active users
- Node.js 18+: Install from nodejs.org with npm or yarn package manager
- WhatsApp Account: Personal WhatsApp installed on smartphone for testing messages
- ngrok: Install ngrok for exposing local development server to internet
Optional for Production:
- WhatsApp Business Account (WABA) for production messaging capabilities beyond Sandbox limitations
- Twilio phone number with WhatsApp capability ($1-$2/month)
- Basic understanding of React, Next.js API routes, and SQL queries
Tutorial Specifications:
- Estimated Completion Time: 45-60 minutes
- Skill Level Required: Intermediate (familiarity with React and REST APIs)
- Development Cost: $0 (free tiers available for all services)
System Architecture
graph TB
subgraph Client Layer
A[Next.js Frontend] --> B[React Components]
B --> C[Supabase Auth UI]
end
subgraph Next.js Server
D[API Routes] --> E[/api/send-message]
D --> F[/api/webhook/inbound]
D --> G[/api/webhook/status]
end
subgraph Supabase Backend
H[PostgreSQL Database] --> I[messages table]
H --> J[users table]
K[Supabase Auth] --> L[JWT Tokens]
end
subgraph Twilio Platform
M[Twilio WhatsApp API]
N[WhatsApp Sandbox/Production]
end
O[User WhatsApp] <--> P[WhatsApp Servers]
P <--> M
M --> F
M --> G
E --> M
F --> H
A --> D
K --> A
D --> H
style Client Layer fill:#e1f5ff,stroke:#01579b,stroke-width:2px
style Next.js Server fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
style Supabase Backend fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
style Twilio Platform fill:#fff3e0,stroke:#e65100,stroke-width:2pxData Flow Explanation:
- Authentication: User logs in through Supabase Auth in Next.js frontend
- Message Sending: Authenticated user sends WhatsApp message via
/api/send-messageAPI route - Twilio Integration: API route uses Twilio SDK to send message through WhatsApp API
- Message Delivery: Twilio delivers message to recipient's WhatsApp account
- Inbound Messages: When recipient replies, Twilio sends webhook POST request to
/api/webhook/inbound - Data Persistence: Webhook handler validates request and stores message in Supabase database
- Real-time Updates: Frontend queries Supabase for message history with real-time subscriptions
- Status Tracking: Delivery status updates received at
/api/webhook/statusendpoint
Expected Outcome:
A production-ready Next.js application featuring Twilio WhatsApp integration, Supabase authentication, and persistent message storage - ready for deployment to Vercel, Netlify, or any Node.js hosting platform.
1. Setting Up Your Next.js Project
Let's create a new Next.js project configured for Twilio WhatsApp integration and Supabase.
1.1 Initialize Next.js Project
Create a new Next.js 14 application with App Router (recommended for modern Next.js development):
npx create-next-app@latest twilio-whatsapp-nextjs --typescript --app --tailwind
cd twilio-whatsapp-nextjsWhen prompted, choose:
- TypeScript: Yes (recommended for type safety)
- ESLint: Yes
- Tailwind CSS: Yes (for UI styling)
- App Router: Yes (required for this tutorial)
- Import alias: No (or customize as preferred)
1.2 Install Required Dependencies
Install Twilio SDK, Supabase client, and necessary utilities:
npm install twilio @supabase/supabase-js @supabase/ssr @supabase/auth-helpers-nextjsInstall development dependencies:
npm install --save-dev ngrokDependency Explanation:
twilio: Official Twilio SDK providing WhatsApp message sending, SMS, and voice capabilities@supabase/supabase-js: Core Supabase client for database queries and authentication@supabase/ssr: Server-side rendering utilities for Next.js with automatic cookie handling@supabase/auth-helpers-nextjs: Middleware and helpers for Next.js authentication flowsngrok: Exposes local development server for Twilio webhook testing
1.3 Project Structure
Your project should have this structure:
twilio-whatsapp-nextjs/
├── app/
│ ├── api/
│ │ ├── send-message/
│ │ │ └── route.ts
│ │ └── webhook/
│ │ ├── inbound/
│ │ │ └── route.ts
│ │ └── status/
│ │ └── route.ts
│ ├── dashboard/
│ │ └── page.tsx
│ ├── login/
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
├── lib/
│ ├── supabase/
│ │ ├── client.ts
│ │ └── server.ts
│ └── twilio.ts
├── .env.local
├── .gitignore
├── next.config.js
├── package.json
└── tsconfig.json
2. Configuring Twilio WhatsApp Sandbox
Before writing code, set up Twilio WhatsApp Sandbox for testing message functionality.
2.1 Create Twilio Account and Get Credentials
- Sign up: Visit twilio.com/try-twilio and create a free account
- Verify phone number: Complete phone verification during signup
- Access Dashboard: Navigate to Twilio Console
- Copy credentials: Find your Account SID and Auth Token on the dashboard homepage
Important: Keep your Auth Token secure - it provides full access to your Twilio account.
2.2 Set Up WhatsApp Sandbox
- Navigate to WhatsApp: In Twilio Console, go to Messaging > Try it out > Send a WhatsApp message
- Activate Sandbox: Click Join Sandbox or acknowledge terms
- Connect your WhatsApp:
- Scan the QR code with your phone's WhatsApp, OR
- Send the join message (e.g.,
join <keyword>) to the displayed Sandbox number
- Confirm connection: You should receive a confirmation message in WhatsApp
- Note Sandbox number: Copy the Sandbox WhatsApp number (e.g.,
+1 415 523 8886)
Sandbox Limitations:
- Messages only work with pre-approved phone numbers (those who joined via QR/keyword)
- 1 message per 3 seconds rate limit
- Pre-approved message templates only for business-initiated messages
- For production, you'll need a WhatsApp Business Account (WABA)
2.3 Configure Webhook URLs (Return After ngrok Setup)
We'll configure these URLs in Step 5 after setting up ngrok:
- Inbound URL:
https://<your-ngrok-url>/api/webhook/inbound - Status URL:
https://<your-ngrok-url>/api/webhook/status
3. Setting Up Supabase Database and Authentication
Configure Supabase for user authentication and message storage.
3.1 Create Supabase Project
- Sign up: Visit supabase.com and create account
- New Project: Click New Project in dashboard
- Configure project:
- Name:
twilio-whatsapp-app - Database Password: Generate secure password (save it!)
- Region: Choose closest to your users
- Pricing Plan: Free (sufficient for development)
- Name:
- Wait for setup: Project creation takes 1-2 minutes
3.2 Get Supabase Credentials
- Navigate to Settings: Click Settings > API in project dashboard
- Copy credentials:
- Project URL:
https://<your-project>.supabase.co - Anon/Public Key:
eyJ...(safe for client-side use) - Service Role Key:
eyJ...(keep secure, server-side only)
- Project URL:
3.3 Create Database Schema
Create tables for storing messages and user data.
- Open SQL Editor: Click SQL Editor in Supabase dashboard
- Create messages table: Run this SQL:
-- Create messages table for WhatsApp conversation history
CREATE TABLE messages (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
from_number TEXT NOT NULL,
to_number TEXT NOT NULL,
message_body TEXT NOT NULL,
message_sid TEXT UNIQUE,
status TEXT DEFAULT 'sent',
direction TEXT NOT NULL CHECK (direction IN ('inbound', 'outbound')),
media_url TEXT,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create index for faster queries
CREATE INDEX idx_messages_user_id ON messages(user_id);
CREATE INDEX idx_messages_created_at ON messages(created_at DESC);
CREATE INDEX idx_messages_message_sid ON messages(message_sid);
-- Enable Row Level Security (RLS)
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
-- Create policy: Users can only see their own messages
CREATE POLICY "Users can view own messages"
ON messages FOR SELECT
USING (auth.uid() = user_id);
-- Create policy: Users can insert their own messages
CREATE POLICY "Users can insert own messages"
ON messages FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Create function to update timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger to auto-update updated_at
CREATE TRIGGER update_messages_updated_at
BEFORE UPDATE ON messages
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();- Create user profiles table (optional): For storing additional user data:
-- Create user_profiles table for extended user information
CREATE TABLE user_profiles (
id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
whatsapp_number TEXT,
display_name TEXT,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own profile"
ON user_profiles FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON user_profiles FOR UPDATE
USING (auth.uid() = id);3.4 Configure Authentication
- Enable Email Auth: Go to Authentication > Providers > Enable Email
- Configure settings:
- Enable email confirmations: OFF (for development)
- Minimum password length: 6 characters
- Optional: Enable OAuth providers (Google, GitHub) for social login
4. Environment Configuration
Create .env.local file in project root with all necessary credentials:
# Twilio Configuration
TWILIO_ACCOUNT_SID=AC################################
TWILIO_AUTH_TOKEN=################################
TWILIO_WHATSAPP_NUMBER=whatsapp:+14155238886
# Supabase Configuration
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ########################
SUPABASE_SERVICE_ROLE_KEY=eyJ########################
# Application Configuration
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Webhook Security (generate random string)
WEBHOOK_SECRET=your-secure-random-string-hereEnvironment Variable Explanations:
TWILIO_ACCOUNT_SID: Your Twilio account identifier from Console dashboardTWILIO_AUTH_TOKEN: Secret authentication token for Twilio API requests (keep secure!)TWILIO_WHATSAPP_NUMBER: Sandbox number in formatwhatsapp:+14155238886(includewhatsapp:prefix)NEXT_PUBLIC_SUPABASE_URL: Your Supabase project URL (publicly accessible)NEXT_PUBLIC_SUPABASE_ANON_KEY: Public anonymous key safe for client-side useSUPABASE_SERVICE_ROLE_KEY: Service role key with admin privileges (server-side only, never expose!)NEXT_PUBLIC_APP_URL: Your application URL (localhost for dev, production URL for deployment)WEBHOOK_SECRET: Random string for validating webhook authenticity (generate withopenssl rand -hex 32)
Security Notes:
- Add
.env.localto.gitignore(should be included by default) - Never commit credentials to version control
- Use environment variables in production hosting (Vercel, Netlify environment settings)
- Rotate credentials if accidentally exposed
5. Implementing Twilio Client and Supabase Utilities
Create helper modules for Twilio SDK and Supabase clients.
5.1 Create Twilio Client (lib/twilio.ts)
// lib/twilio.ts
import twilio from 'twilio';
// Validate environment variables
if (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {
throw new Error('Missing Twilio credentials in environment variables');
}
// Initialize Twilio client with credentials
export const twilioClient = twilio(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN
);
// Twilio WhatsApp number from environment
export const TWILIO_WHATSAPP_NUMBER = process.env.TWILIO_WHATSAPP_NUMBER;
// Helper function to send WhatsApp message
export async function sendWhatsAppMessage(
to: string,
body: string,
mediaUrl?: string
) {
try {
const message = await twilioClient.messages.create({
from: TWILIO_WHATSAPP_NUMBER,
to: `whatsapp:${to}`, // Ensure whatsapp: prefix
body: body,
...(mediaUrl && { mediaUrl: [mediaUrl] }), // Optional media attachment
});
return {
success: true,
messageSid: message.sid,
status: message.status,
};
} catch (error) {
console.error('Twilio send error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
// Helper to validate Twilio webhook signature (security)
export function validateTwilioSignature(
signature: string,
url: string,
params: Record<string, any>
): boolean {
const authToken = process.env.TWILIO_AUTH_TOKEN;
if (!authToken) return false;
return twilio.validateRequest(authToken, signature, url, params);
}5.2 Create Supabase Server Client (lib/supabase/server.ts)
// lib/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { cookies } from 'next/headers';
export function createClient() {
const cookieStore = 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;
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options });
} catch (error) {
// Handle cookie setting errors in Server Components
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: '', ...options });
} catch (error) {
// Handle cookie removal errors
}
},
},
}
);
}
// Admin client with service role (for webhook handlers)
export function createAdminClient() {
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // Service role for admin access
{
cookies: {
get() { return undefined; },
set() {},
remove() {},
},
}
);
}5.3 Create Supabase Client Component Client (lib/supabase/client.ts)
// 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!
);
}6. Building API Routes for WhatsApp Messaging
Implement Next.js API routes for sending messages and handling webhooks.
6.1 Send Message API Route (app/api/send-message/route.ts)
// app/api/send-message/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { sendWhatsAppMessage } from '@/lib/twilio';
export async function POST(request: NextRequest) {
try {
// Get authenticated user from Supabase
const supabase = createClient();
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json(
{ error: 'Unauthorized - Please log in' },
{ status: 401 }
);
}
// Parse request body
const body = await request.json();
const { to, message, mediaUrl } = body;
// Validate input
if (!to || !message) {
return NextResponse.json(
{ error: 'Missing required fields: to, message' },
{ status: 400 }
);
}
// Validate phone number format (basic E.164 validation)
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
if (!phoneRegex.test(to)) {
return NextResponse.json(
{ error: 'Invalid phone number format. Use E.164 format (e.g., +1234567890)' },
{ status: 400 }
);
}
// Send WhatsApp message via Twilio
const result = await sendWhatsAppMessage(to, message, mediaUrl);
if (!result.success) {
return NextResponse.json(
{ error: `Failed to send message: ${result.error}` },
{ status: 500 }
);
}
// Store message in Supabase database
const { data: messageData, error: dbError } = await supabase
.from('messages')
.insert({
user_id: user.id,
from_number: process.env.TWILIO_WHATSAPP_NUMBER || '',
to_number: `whatsapp:${to}`,
message_body: message,
message_sid: result.messageSid,
status: result.status || 'sent',
direction: 'outbound',
...(mediaUrl && { media_url: mediaUrl }),
})
.select()
.single();
if (dbError) {
console.error('Database insert error:', dbError);
// Message sent successfully but failed to save to DB
return NextResponse.json(
{
success: true,
messageSid: result.messageSid,
warning: 'Message sent but failed to save to database'
},
{ status: 200 }
);
}
return NextResponse.json({
success: true,
messageSid: result.messageSid,
message: messageData,
});
} catch (error) {
console.error('Send message error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}6.2 Inbound Webhook Handler (app/api/webhook/inbound/route.ts)
// app/api/webhook/inbound/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createAdminClient } from '@/lib/supabase/server';
import { validateTwilioSignature } from '@/lib/twilio';
export async function POST(request: NextRequest) {
try {
// Get webhook data from Twilio
const formData = await request.formData();
const params: Record<string, any> = {};
formData.forEach((value, key) => {
params[key] = value.toString();
});
// Validate Twilio webhook signature for security
const signature = request.headers.get('x-twilio-signature') || '';
const url = request.url;
if (!validateTwilioSignature(signature, url, params)) {
console.error('Invalid Twilio webhook signature');
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 403 }
);
}
// Extract message data
const {
MessageSid,
From,
To,
Body,
NumMedia,
MediaUrl0,
} = params;
console.log('Inbound WhatsApp message:', { From, Body });
// Store inbound message in database using admin client
// (no user authentication required for webhooks)
const supabase = createAdminClient();
const { error: dbError } = await supabase
.from('messages')
.insert({
from_number: From,
to_number: To,
message_body: Body || '',
message_sid: MessageSid,
status: 'received',
direction: 'inbound',
media_url: NumMedia && parseInt(NumMedia) > 0 ? MediaUrl0 : null,
// Note: user_id is null for inbound messages from unknown users
// You can implement logic to match phone numbers to users if needed
});
if (dbError) {
console.error('Database insert error:', dbError);
// Still return 200 to Twilio to avoid retries
}
// Optional: Implement auto-reply logic here
// const autoReply = `Thank you for your message! We'll get back to you soon.`;
// await sendWhatsAppMessage(From.replace('whatsapp:', ''), autoReply);
// Always return 200 OK to Twilio to acknowledge receipt
// Otherwise Twilio will retry sending the webhook
return new NextResponse('Message received', { status: 200 });
} catch (error) {
console.error('Inbound webhook error:', error);
// Return 200 even on error to prevent Twilio retries
return new NextResponse('Error processed', { status: 200 });
}
}6.3 Status Webhook Handler (app/api/webhook/status/route.ts)
// app/api/webhook/status/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createAdminClient } from '@/lib/supabase/server';
import { validateTwilioSignature } from '@/lib/twilio';
export async function POST(request: NextRequest) {
try {
// Parse Twilio webhook data
const formData = await request.formData();
const params: Record<string, any> = {};
formData.forEach((value, key) => {
params[key] = value.toString();
});
// Validate webhook signature
const signature = request.headers.get('x-twilio-signature') || '';
const url = request.url;
if (!validateTwilioSignature(signature, url, params)) {
console.error('Invalid status webhook signature');
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 403 }
);
}
// Extract status update data
const {
MessageSid,
MessageStatus,
ErrorCode,
ErrorMessage,
} = params;
console.log('Message status update:', { MessageSid, MessageStatus });
// Update message status in database
const supabase = createAdminClient();
const updateData: Record<string, any> = {
status: MessageStatus,
};
// Add error information if present
if (ErrorCode) {
updateData.error_code = ErrorCode;
updateData.error_message = ErrorMessage;
}
const { error: dbError } = await supabase
.from('messages')
.update(updateData)
.eq('message_sid', MessageSid);
if (dbError) {
console.error('Database update error:', dbError);
}
// Return 200 OK to acknowledge status update
return new NextResponse('Status updated', { status: 200 });
} catch (error) {
console.error('Status webhook error:', error);
return new NextResponse('Error processed', { status: 200 });
}
}7. Testing the Integration
Set up ngrok for local webhook testing and test the complete flow.
7.1 Start ngrok Tunnel
- Open new terminal window in your project directory
- Start ngrok to expose port 3000:
ngrok http 3000- Copy HTTPS URL from ngrok output (e.g.,
https://abc123.ngrok.io) - Keep ngrok running in background terminal
7.2 Configure Twilio Webhooks
- Return to Twilio Console > Messaging > Settings > WhatsApp Sandbox Settings
- Set webhook URLs:
- When a message comes in:
https://abc123.ngrok.io/api/webhook/inbound - Status callback URL:
https://abc123.ngrok.io/api/webhook/status
- When a message comes in:
- HTTP Method: POST (default)
- Click Save
7.3 Run Development Server
npm run devServer should start at http://localhost:3000
7.4 Test Message Sending
- Authenticate a test user (implement login page or use Supabase Auth UI)
- Send test message via your application frontend or using curl:
curl -X POST http://localhost:3000/api/send-message \
-H "Content-Type: application/json" \
-H "Cookie: your-auth-cookie" \
-d '{
"to": "+1234567890",
"message": "Hello from my Next.js WhatsApp app!"
}'- Check your WhatsApp - you should receive the message
- Reply to the message from your WhatsApp
- Check terminal logs - you should see the inbound webhook data
- Check Supabase - verify messages are stored in database
For more information on testing webhooks, see our guide on debugging Twilio webhooks.
8. Security Best Practices
Implement production-ready security measures for your WhatsApp integration.
8.1 Webhook Signature Verification
Always validate Twilio webhook signatures to prevent unauthorized requests:
// Already implemented in our webhook handlers
if (!validateTwilioSignature(signature, url, params)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 403 });
}Why it matters: Without signature validation, anyone could send fake webhook requests to your API.
8.2 Authentication and Authorization
Require authentication for all message-sending operations:
// Check user authentication before sending messages
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}8.3 Rate Limiting
Implement rate limiting to prevent abuse (example using Next.js middleware):
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const rateLimit = new Map<string, { count: number; resetTime: number }>();
export function middleware(request: NextRequest) {
const ip = request.ip || 'unknown';
const now = Date.now();
const limit = 10; // 10 requests
const window = 60000; // per minute
const userLimit = rateLimit.get(ip);
if (!userLimit || now > userLimit.resetTime) {
rateLimit.set(ip, { count: 1, resetTime: now + window });
return NextResponse.next();
}
if (userLimit.count >= limit) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{ status: 429 }
);
}
userLimit.count++;
return NextResponse.next();
}
export const config = {
matcher: '/api/send-message/:path*',
};8.4 Input Validation and Sanitization
Validate and sanitize all user inputs:
// Validate phone number format
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
if (!phoneRegex.test(to)) {
return NextResponse.json({ error: 'Invalid phone number' }, { status: 400 });
}
// Sanitize message content (prevent injection attacks)
const sanitizedMessage = message.trim().slice(0, 1600); // WhatsApp limit8.5 Environment Variable Security
- Never commit
.env.localto version control - Use different credentials for development/production
- Rotate credentials regularly
- Use secrets management (Vercel Secrets, AWS Secrets Manager) in production
For comprehensive security guidance, see our API security best practices guide.
9. Deployment to Production
Deploy your Next.js WhatsApp application to a production hosting platform.
9.1 Vercel Deployment (Recommended)
Vercel is the recommended platform for Next.js applications:
- Install Vercel CLI:
npm install -g vercel- Deploy application:
vercel-
Set environment variables in Vercel dashboard:
- Go to Settings > Environment Variables
- Add all variables from
.env.local - Ensure
NEXT_PUBLIC_APP_URLuses your production domain
-
Update Twilio webhooks with production URLs:
- Inbound:
https://your-app.vercel.app/api/webhook/inbound - Status:
https://your-app.vercel.app/api/webhook/status
- Inbound:
9.2 Alternative Hosting Options
Netlify:
npm run build
netlify deploy --prodRailway:
railway login
railway upAWS/Google Cloud/Azure: Deploy as containerized application using Docker
9.3 Production Checklist
- Environment variables configured in hosting platform
- Twilio webhook URLs updated to production domain
- Supabase Row Level Security (RLS) policies enabled
- HTTPS enabled on custom domain
- Rate limiting implemented
- Error tracking configured (Sentry, LogRocket)
- Monitoring and logging set up
- WhatsApp Business Account (WABA) configured for production messaging
For detailed deployment instructions, see our Next.js deployment guide.
10. Troubleshooting Common Issues
Issue: Webhooks not receiving data
Symptoms: Inbound messages not appearing in database, no logs in console
Solutions:
- Verify ngrok is running and URL matches Twilio webhook configuration
- Check Twilio webhook logs in Console > Monitor > Logs > Webhooks
- Ensure webhook URLs use HTTPS (not HTTP)
- Check firewall settings aren't blocking requests
Issue: "Invalid signature" errors
Symptoms: Webhook returns 403 Forbidden, "Invalid signature" in logs
Solutions:
- Verify
TWILIO_AUTH_TOKENin environment variables is correct - Check webhook URL exactly matches what's configured in Twilio (including trailing slashes)
- Ensure request body isn't being modified by middleware
- For local development, ensure ngrok free plan limitations aren't affecting headers
Issue: Messages not sending
Symptoms: API returns error, messages don't appear in WhatsApp
Solutions:
- Verify Twilio credentials (
ACCOUNT_SID,AUTH_TOKEN) are correct - Check recipient number is allowlisted in WhatsApp Sandbox
- Verify phone number format includes country code (E.164: +1234567890)
- Check Twilio account balance/credits
- Review Twilio error codes in response for specific issues
Issue: Database not storing messages
Symptoms: Messages send successfully but don't appear in Supabase
Solutions:
- Check Supabase credentials in
.env.local - Verify Row Level Security (RLS) policies allow inserts
- Check database table schema matches insert query
- Review Supabase logs in Dashboard > Logs
Issue: Authentication failing
Symptoms: "Unauthorized" errors, can't send messages
Solutions:
- Verify user is logged in and session is valid
- Check
createClient()is properly configured in API routes - Ensure cookies are being set correctly (check browser DevTools)
- Verify
NEXT_PUBLIC_SUPABASE_ANON_KEYis correct
For more troubleshooting help, visit our troubleshooting guide or contact support.
Conclusion
You've successfully built a production-ready WhatsApp messaging application integrating Twilio's WhatsApp API, Next.js serverless API routes, and Supabase for authentication and data persistence. Your application can now send and receive WhatsApp messages, authenticate users securely, and store conversation history in a PostgreSQL database.
Next Steps
Enhance your application:
- Message templates: Implement Twilio WhatsApp templates for business-initiated messages
- Rich media: Add support for images, videos, and documents
- Chatbot integration: Connect AI chatbots or NLP services for automated responses
- Real-time UI: Implement Supabase real-time subscriptions for live message updates
- Analytics: Add message tracking and analytics dashboards
Production migration:
- Apply for WhatsApp Business Account (WABA)
- Purchase Twilio phone number with WhatsApp capability
- Set up monitoring and alerting
- Implement advanced security measures
Related Resources
- Twilio WhatsApp API Documentation
- Next.js API Routes Guide
- Supabase Authentication Docs
- WhatsApp Business API Best Practices
- Node.js SMS Integration Tutorial
Questions or issues? Join our developer community or contact support for assistance with your WhatsApp integration project.
Frequently Asked Questions
How to send WhatsApp messages with Node.js?
Use the Vonage Messages API with the Node.js SDK and Express. Set up webhooks to receive incoming messages and send replies using the WhatsAppText object. The Vonage Node SDK simplifies interactions with the API and includes modules for messages and JWT verification.
What is the Vonage Messages API?
The Vonage Messages API is a unified API for sending and receiving messages across multiple channels including WhatsApp, SMS, and MMS. It provides a single interface for integrating messaging functionality into your applications.
Why does Vonage use webhooks for WhatsApp?
Vonage uses webhooks to deliver real-time updates about incoming messages and message status to your application. This allows your server to respond to events without constantly polling the API, improving efficiency.
When should I remove apiHost from Vonage client?
Remove the `apiHost` option from the Vonage client initialization when transitioning from the Sandbox environment to a production setup with a purchased Vonage number and a WhatsApp Business Account.
Can I use Node.js version 18 with Vonage?
Node.js version 18 LTS is generally compatible with the Vonage Messages API and SDK. However, testing is recommended, and using version 20 or higher is preferred for access to the latest features and better performance.
How to verify Vonage webhook signatures in Express?
Use the `@vonage/jwt` library's `verifySignature` function with your Vonage API Signature Secret. This ensures incoming webhooks are genuinely from Vonage, enhancing security. Implement this as middleware in your Express routes.
What is ngrok used for with Vonage webhooks?
ngrok creates a secure tunnel to expose your local development server to the internet, allowing Vonage to send webhooks to your application during development.
How to set up the Vonage WhatsApp Sandbox?
In the Vonage Dashboard, navigate to Developer Tools > Messages API Sandbox. Scan the QR code with your WhatsApp account or send the specified message to allowlist your number for testing.
What is the VONAGE_API_SIGNATURE_SECRET used for?
The `VONAGE_API_SIGNATURE_SECRET` is crucial for verifying the authenticity of incoming webhooks. It confirms requests originate from Vonage, preventing unauthorized access to your application.
How to handle inbound WhatsApp messages with Express?
Create a POST route in your Express app (e.g., `/webhooks/inbound`) to handle incoming messages. Use the `express.json()` middleware to parse the request body containing the message data. The article provides an example of an Express route setup.
What are WhatsApp Message Templates?
WhatsApp Message Templates are pre-approved message formats required for sending unsolicited notifications to users more than 24 hours after their last interaction. They are different from session messages and require separate setup.
Why do I need a Vonage Application ID?
The Vonage Application ID uniquely identifies your application within the Vonage platform. It's required when initializing the Vonage client in your code to link your code to your Vonage account and configuration.
How to configure Vonage webhooks for WhatsApp?
In the Vonage Dashboard, under your application's settings, configure the Inbound and Status URLs for the Messages capability. These URLs should point to your application's endpoints for receiving message and status updates.
What is the purpose of dotenv in the Node.js project?
The `dotenv` module loads environment variables from a `.env` file into `process.env`, allowing you to store and manage sensitive configuration data like API keys and secrets securely.