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.
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 routes
ngrok - 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:2px
Data Flow Explanation:
Authentication: User logs in through Supabase Auth in Next.js frontend
Message Sending: Authenticated user sends WhatsApp message via /api/send-message API 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/status endpoint
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):
Bash
npx create-next-app@latest twilio-whatsapp-nextjs --typescript --app --tailwind
cd twilio-whatsapp-nextjs
When 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:
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)
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:
SQL
-- Create messages table for WhatsApp conversation historyCREATE TABLE messages (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
created_at TIMESTAMPWITHTIME ZONE DEFAULT NOW(),
user_id UUID REFERENCES auth.users(id) ONDELETE 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 NULLCHECK (direction IN ('inbound', 'outbound')),
media_url TEXT,
updated_at TIMESTAMPWITHTIME ZONE DEFAULT NOW()
);
-- Create index for faster queriesCREATE 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 messagesCREATE POLICY "Users can view own messages"
ON messages FORSELECTUSING (auth.uid() = user_id);
-- Create policy: Users can insert their own messagesCREATE POLICY "Users can insert own messages"
ON messages FORINSERTWITHCHECK (auth.uid() = user_id);
-- Create function to update timestampCREATEOR REPLACE FUNCTION update_updated_at_column()
RETURNSTRIGGERAS $$
BEGIN
NEW.updated_at = NOW();
RETURNNEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger to auto-update updated_atCREATETRIGGER update_messages_updated_at
BEFORE UPDATEON messages
FOREACHROWEXECUTEFUNCTION update_updated_at_column();
Create user profiles table (optional): For storing additional user data:
SQL
-- Create user_profiles table for extended user informationCREATE TABLE user_profiles (
id UUID REFERENCES auth.users(id) ONDELETE CASCADE PRIMARY KEY,
created_at TIMESTAMPWITHTIME ZONE DEFAULT NOW(),
whatsapp_number TEXT,
display_name TEXT,
updated_at TIMESTAMPWITHTIME ZONE DEFAULT NOW()
);
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own profile"
ON user_profiles FORSELECTUSING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON user_profiles FORUPDATEUSING (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:
Check createClient() is properly configured in API routes
Ensure cookies are being set correctly (check browser DevTools)
Verify NEXT_PUBLIC_SUPABASE_ANON_KEY is 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.
Questions or issues? Join our developer community or contact support for assistance with your WhatsApp integration project.
Frequently asked questions
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.
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.
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.
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.
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.
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.
ngrok creates a secure tunnel to expose your local development server to the internet, allowing Vonage to send webhooks to your application during development.
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.
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.
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.
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.
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.
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.
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.
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:
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:
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 developmentPrerequisites
Required Accounts and Tools:
Optional for Production:
Tutorial Specifications:
System Architecture
Data Flow Explanation:
/api/send-messageAPI route/api/webhook/inbound/api/webhook/statusendpointExpected 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:
1.2 Install Required Dependencies
Install Twilio SDK, Supabase client, and necessary utilities:
Install development dependencies:
Dependency 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 testing1.3 Project Structure
Your project should have this structure:
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
Important: Keep your Auth Token secure - it provides full access to your Twilio account.
2.2 Set Up WhatsApp Sandbox
join <keyword>) to the displayed Sandbox number+1 415 523 8886)Sandbox Limitations:
2.3 Configure Webhook URLs (Return After ngrok Setup)
We'll configure these URLs in Step 5 after setting up ngrok:
https://<your-ngrok-url>/api/webhook/inboundhttps://<your-ngrok-url>/api/webhook/status3. Setting Up Supabase Database and Authentication
Configure Supabase for user authentication and message storage.
3.1 Create Supabase Project
twilio-whatsapp-app3.2 Get Supabase Credentials
https://<your-project>.supabase.coeyJ...(safe for client-side use)eyJ...(keep secure, server-side only)3.3 Create Database Schema
Create tables for storing messages and user data.
-- 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 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
4. Environment Configuration
Create
.env.localfile 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:
.env.localto.gitignore(should be included by default)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
https://abc123.ngrok.io)7.2 Configure Twilio Webhooks
https://abc123.ngrok.io/api/webhook/inboundhttps://abc123.ngrok.io/api/webhook/status7.3 Run Development Server
Server should start at
http://localhost:30007.4 Test Message Sending
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!" }'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
.env.localto version controlFor 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:
Set environment variables in Vercel dashboard:
.env.localNEXT_PUBLIC_APP_URLuses your production domainUpdate Twilio webhooks with production URLs:
https://your-app.vercel.app/api/webhook/inboundhttps://your-app.vercel.app/api/webhook/status9.2 Alternative Hosting Options
Netlify:
Railway:
AWS/Google Cloud/Azure: Deploy as containerized application using Docker
9.3 Production Checklist
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:
Issue: "Invalid signature" errors
Symptoms: Webhook returns 403 Forbidden, "Invalid signature" in logs
Solutions:
TWILIO_AUTH_TOKENin environment variables is correctIssue: Messages not sending
Symptoms: API returns error, messages don't appear in WhatsApp
Solutions:
ACCOUNT_SID,AUTH_TOKEN) are correctIssue: Database not storing messages
Symptoms: Messages send successfully but don't appear in Supabase
Solutions:
.env.localIssue: Authentication failing
Symptoms: "Unauthorized" errors, can't send messages
Solutions:
createClient()is properly configured in API routesNEXT_PUBLIC_SUPABASE_ANON_KEYis correctFor 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:
Production migration:
Related Resources
Questions or issues? Join our developer community or contact support for assistance with your WhatsApp integration project.