Prerequisites
- Node.js 16+ installed
- Supabase account (create one here)
- WhatsApp Business API credentials
- Basic knowledge of TypeScript/JavaScript
Step 1: Create Supabase Database
1.1 Create Project
- Go to Supabase Dashboard
- Click “New Project”
- Enter project name and database password
- Choose a region close to your users
- Click “Create new project”
1.2 Run SQL Schema
- Go to SQL Editor in your Supabase dashboard
- Click “New Query”
- Copy the entire content from
/sql/supabase-schema.sql(located in the SDK repository) - Paste and click “Run”
View Complete SQL Schema
View Complete SQL Schema
Copy
-- WhatsApp SDK Storage Schema for Supabase
-- Version: 1.0.0
-- WhatsApp SDK Supabase Schema
-- This file contains the complete database schema for WhatsApp message storage in Supabase
-- You can run this manually if you prefer not to use autoCreateTables: true
-- Enable necessary extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Create conversations table
CREATE TABLE IF NOT EXISTS whatsapp_conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
phone_number VARCHAR(20) NOT NULL,
business_phone_id VARCHAR(20) NOT NULL,
last_message_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
message_count INTEGER DEFAULT 0,
unread_count INTEGER DEFAULT 0,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Constraints
CONSTRAINT uk_phone_business UNIQUE(phone_number, business_phone_id)
);
-- Create messages table
CREATE TABLE IF NOT EXISTS whatsapp_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
whatsapp_message_id VARCHAR(255) UNIQUE NOT NULL,
conversation_id UUID REFERENCES whatsapp_conversations(id) ON DELETE CASCADE,
phone_number_id VARCHAR(20) NOT NULL,
from_phone VARCHAR(20) NOT NULL,
to_phone VARCHAR(20) NOT NULL,
message_type VARCHAR(20) NOT NULL,
content JSONB NOT NULL DEFAULT '{}',
reply_to_message_id UUID REFERENCES whatsapp_messages(id) ON DELETE SET NULL,
whatsapp_reply_to_id VARCHAR(255),
timestamp TIMESTAMPTZ NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'sent',
direction VARCHAR(10) NOT NULL CHECK (direction IN ('incoming', 'outgoing')),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Constraints
CONSTRAINT ck_message_status CHECK (status IN ('sent', 'delivered', 'read', 'failed')),
CONSTRAINT ck_message_type CHECK (message_type IN (
'text', 'image', 'video', 'audio', 'document', 'location',
'contacts', 'sticker', 'reaction', 'interactive', 'template'
))
);
-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON whatsapp_messages(conversation_id);
CREATE INDEX IF NOT EXISTS idx_messages_from_phone ON whatsapp_messages(from_phone);
CREATE INDEX IF NOT EXISTS idx_messages_to_phone ON whatsapp_messages(to_phone);
CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON whatsapp_messages(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_messages_type ON whatsapp_messages(message_type);
CREATE INDEX IF NOT EXISTS idx_messages_direction ON whatsapp_messages(direction);
CREATE INDEX IF NOT EXISTS idx_messages_status ON whatsapp_messages(status);
CREATE INDEX IF NOT EXISTS idx_messages_reply ON whatsapp_messages(reply_to_message_id);
CREATE INDEX IF NOT EXISTS idx_messages_whatsapp_reply ON whatsapp_messages(whatsapp_reply_to_id);
CREATE INDEX IF NOT EXISTS idx_messages_whatsapp_id ON whatsapp_messages(whatsapp_message_id);
-- Full-text search index for content
CREATE INDEX IF NOT EXISTS idx_messages_content_search ON whatsapp_messages
USING GIN (to_tsvector('english', COALESCE(content->>'text', '')));
-- Additional useful indexes
CREATE INDEX IF NOT EXISTS idx_messages_phone_timestamp ON whatsapp_messages(from_phone, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_messages_conversation_timestamp ON whatsapp_messages(conversation_id, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_messages_direction_timestamp ON whatsapp_messages(direction, timestamp DESC);
-- Conversations indexes
CREATE INDEX IF NOT EXISTS idx_conversations_phone ON whatsapp_conversations(phone_number);
CREATE INDEX IF NOT EXISTS idx_conversations_business ON whatsapp_conversations(business_phone_id);
CREATE INDEX IF NOT EXISTS idx_conversations_last_message ON whatsapp_conversations(last_message_at DESC);
-- Functions for maintaining conversation metadata
CREATE OR REPLACE FUNCTION update_conversation_on_message()
RETURNS TRIGGER AS $$
BEGIN
-- Update conversation activity and message count
INSERT INTO whatsapp_conversations (phone_number, business_phone_id, last_message_at, message_count, updated_at)
VALUES (
CASE
WHEN NEW.direction = 'incoming' THEN NEW.from_phone
ELSE NEW.to_phone
END,
NEW.phone_number_id,
NEW.timestamp,
1,
NOW()
)
ON CONFLICT (phone_number, business_phone_id)
DO UPDATE SET
last_message_at = GREATEST(whatsapp_conversations.last_message_at, NEW.timestamp),
message_count = whatsapp_conversations.message_count + 1,
updated_at = NOW();
-- Update conversation_id in the message if it was just created
IF NEW.conversation_id IS NULL THEN
UPDATE whatsapp_messages
SET conversation_id = (
SELECT id FROM whatsapp_conversations
WHERE phone_number = CASE
WHEN NEW.direction = 'incoming' THEN NEW.from_phone
ELSE NEW.to_phone
END
AND business_phone_id = NEW.phone_number_id
)
WHERE id = NEW.id;
END IF;
-- If this is an incoming message, increment unread count
IF NEW.direction = 'incoming' THEN
UPDATE whatsapp_conversations
SET unread_count = unread_count + 1
WHERE phone_number = NEW.from_phone
AND business_phone_id = NEW.phone_number_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger to automatically update conversation metadata
DROP TRIGGER IF EXISTS update_conversation_trigger ON whatsapp_messages;
CREATE TRIGGER update_conversation_trigger
AFTER INSERT ON whatsapp_messages
FOR EACH ROW
EXECUTE FUNCTION update_conversation_on_message();
-- Function for marking messages as read and updating unread count
CREATE OR REPLACE FUNCTION mark_conversation_read(conversation_uuid UUID)
RETURNS void AS $$
BEGIN
UPDATE whatsapp_conversations
SET unread_count = 0, updated_at = NOW()
WHERE id = conversation_uuid;
END;
$$ LANGUAGE plpgsql;
-- Function for cleaning up old messages
CREATE OR REPLACE FUNCTION cleanup_old_messages(retention_days INTEGER DEFAULT 90)
RETURNS INTEGER AS $$
DECLARE
deleted_count INTEGER;
BEGIN
-- Delete messages older than retention period
WITH deleted AS (
DELETE FROM whatsapp_messages
WHERE created_at < NOW() - (retention_days || ' days')::INTERVAL
RETURNING conversation_id
)
SELECT count(*) INTO deleted_count FROM deleted;
-- Update conversation message counts
UPDATE whatsapp_conversations
SET message_count = (
SELECT COUNT(*)
FROM whatsapp_messages
WHERE conversation_id = whatsapp_conversations.id
);
RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;
-- Function for getting conversation statistics
CREATE OR REPLACE FUNCTION get_conversation_stats(phone_number_param VARCHAR)
RETURNS TABLE(
total_messages BIGINT,
incoming_messages BIGINT,
outgoing_messages BIGINT,
first_message_at TIMESTAMPTZ,
last_message_at TIMESTAMPTZ,
avg_response_time_minutes NUMERIC
) AS $$
BEGIN
RETURN QUERY
SELECT
COUNT(*) as total_messages,
COUNT(*) FILTER (WHERE direction = 'incoming') as incoming_messages,
COUNT(*) FILTER (WHERE direction = 'outgoing') as outgoing_messages,
MIN(timestamp) as first_message_at,
MAX(timestamp) as last_message_at,
AVG(EXTRACT(EPOCH FROM (
LEAD(timestamp) OVER (ORDER BY timestamp) - timestamp
)) / 60) as avg_response_time_minutes
FROM whatsapp_messages m
JOIN whatsapp_conversations c ON m.conversation_id = c.id
WHERE c.phone_number = phone_number_param;
END;
$$ LANGUAGE plpgsql;
-- Function for searching messages with full-text search
CREATE OR REPLACE FUNCTION search_messages(
search_text TEXT DEFAULT NULL,
phone_number_filter VARCHAR DEFAULT NULL,
message_type_filter VARCHAR DEFAULT NULL,
direction_filter VARCHAR DEFAULT NULL,
date_from_filter TIMESTAMPTZ DEFAULT NULL,
date_to_filter TIMESTAMPTZ DEFAULT NULL,
limit_count INTEGER DEFAULT 50,
offset_count INTEGER DEFAULT 0
)
RETURNS TABLE(
id UUID,
whatsapp_message_id VARCHAR,
conversation_id UUID,
from_phone VARCHAR,
to_phone VARCHAR,
message_type VARCHAR,
content JSONB,
msg_timestamp TIMESTAMPTZ,
direction VARCHAR,
rank REAL
) AS $$
BEGIN
RETURN QUERY
SELECT
m.id,
m.whatsapp_message_id,
m.conversation_id,
m.from_phone,
m.to_phone,
m.message_type,
m.content,
m.timestamp AS msg_timestamp,
m.direction,
CASE
WHEN search_text IS NOT NULL THEN
ts_rank(to_tsvector('english', COALESCE(m.content->>'text', '')), plainto_tsquery('english', search_text))
ELSE 1.0
END as rank
FROM whatsapp_messages m
LEFT JOIN whatsapp_conversations c ON m.conversation_id = c.id
WHERE
(search_text IS NULL OR to_tsvector('english', COALESCE(m.content->>'text', '')) @@ plainto_tsquery('english', search_text))
AND (phone_number_filter IS NULL OR c.phone_number = phone_number_filter)
AND (message_type_filter IS NULL OR m.message_type = message_type_filter)
AND (direction_filter IS NULL OR m.direction = direction_filter)
AND (date_from_filter IS NULL OR m.timestamp >= date_from_filter)
AND (date_to_filter IS NULL OR m.timestamp <= date_to_filter)
ORDER BY
CASE
WHEN search_text IS NOT NULL THEN
ts_rank(to_tsvector('english', COALESCE(m.content->>'text', '')), plainto_tsquery('english', search_text))
ELSE 0
END DESC,
m.timestamp DESC
LIMIT limit_count
OFFSET offset_count;
END;
$$ LANGUAGE plpgsql;
-- Views for common queries
CREATE OR REPLACE VIEW message_threads AS
SELECT
m.id,
m.whatsapp_message_id,
m.conversation_id,
m.from_phone,
m.content,
m.timestamp,
m.direction,
m.reply_to_message_id,
parent.content as parent_content,
parent.timestamp as parent_timestamp,
COALESCE(reply_count.count, 0) as reply_count
FROM whatsapp_messages m
LEFT JOIN whatsapp_messages parent ON m.reply_to_message_id = parent.id
LEFT JOIN (
SELECT reply_to_message_id, COUNT(*) as count
FROM whatsapp_messages
WHERE reply_to_message_id IS NOT NULL
GROUP BY reply_to_message_id
) reply_count ON m.id = reply_count.reply_to_message_id
ORDER BY m.timestamp DESC;
CREATE OR REPLACE VIEW conversation_summary AS
SELECT
c.*,
COALESCE(m.content->>'text',
CASE
WHEN m.message_type = 'image' THEN '[Image]'
WHEN m.message_type = 'video' THEN '[Video]'
WHEN m.message_type = 'audio' THEN '[Audio]'
WHEN m.message_type = 'document' THEN '[Document]'
WHEN m.message_type = 'location' THEN '[Location]'
WHEN m.message_type = 'sticker' THEN '[Sticker]'
WHEN m.message_type = 'reaction' THEN '[Reaction]'
ELSE '[' || m.message_type || ']'
END
) as last_message_preview,
m.message_type as last_message_type,
m.direction as last_message_direction,
m.timestamp as last_message_timestamp
FROM whatsapp_conversations c
LEFT JOIN whatsapp_messages m ON m.conversation_id = c.id
AND m.timestamp = c.last_message_at
ORDER BY c.last_message_at DESC;
-- Analytics view
CREATE OR REPLACE VIEW message_analytics AS
SELECT
DATE_TRUNC('day', timestamp) as date,
COUNT(*) as total_messages,
COUNT(*) FILTER (WHERE direction = 'incoming') as incoming_messages,
COUNT(*) FILTER (WHERE direction = 'outgoing') as outgoing_messages,
COUNT(DISTINCT conversation_id) as active_conversations,
COUNT(DISTINCT CASE WHEN direction = 'incoming' THEN from_phone END) as unique_customers
FROM whatsapp_messages
WHERE timestamp >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY DATE_TRUNC('day', timestamp)
ORDER BY date DESC;
-- Optional: Row Level Security (RLS) setup
-- Uncomment these if you want to enable RLS
/*
-- Enable RLS on both tables
ALTER TABLE whatsapp_messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE whatsapp_conversations ENABLE ROW LEVEL SECURITY;
-- RLS Policies for messages table
-- These assume you have a user_id field in metadata
CREATE POLICY "Users can view their own messages" ON whatsapp_messages
FOR SELECT USING (
auth.uid()::text = metadata->>'user_id' OR
auth.uid()::text = metadata->>'business_owner_id'
);
CREATE POLICY "Users can insert their own messages" ON whatsapp_messages
FOR INSERT WITH CHECK (
auth.uid()::text = metadata->>'user_id' OR
auth.uid()::text = metadata->>'business_owner_id'
);
CREATE POLICY "Users can update their own messages" ON whatsapp_messages
FOR UPDATE USING (
auth.uid()::text = metadata->>'user_id' OR
auth.uid()::text = metadata->>'business_owner_id'
);
-- RLS Policies for conversations table
CREATE POLICY "Users can view their own conversations" ON whatsapp_conversations
FOR SELECT USING (
auth.uid()::text = metadata->>'user_id' OR
auth.uid()::text = metadata->>'business_owner_id'
);
CREATE POLICY "Users can insert their own conversations" ON whatsapp_conversations
FOR INSERT WITH CHECK (
auth.uid()::text = metadata->>'user_id' OR
auth.uid()::text = metadata->>'business_owner_id'
);
CREATE POLICY "Users can update their own conversations" ON whatsapp_conversations
FOR UPDATE USING (
auth.uid()::text = metadata->>'user_id' OR
auth.uid()::text = metadata->>'business_owner_id'
);
*/
-- Grant permissions to authenticated users
-- Adjust these based on your security requirements
GRANT SELECT, INSERT, UPDATE, DELETE ON whatsapp_messages TO authenticated;
GRANT SELECT, INSERT, UPDATE, DELETE ON whatsapp_conversations TO authenticated;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO authenticated;
-- Grant permissions for the views and functions
GRANT SELECT ON message_threads TO authenticated;
GRANT SELECT ON conversation_summary TO authenticated;
GRANT SELECT ON message_analytics TO authenticated;
GRANT EXECUTE ON FUNCTION update_conversation_on_message() TO authenticated;
GRANT EXECUTE ON FUNCTION mark_conversation_read(UUID) TO authenticated;
GRANT EXECUTE ON FUNCTION cleanup_old_messages(INTEGER) TO authenticated;
GRANT EXECUTE ON FUNCTION get_conversation_stats(VARCHAR) TO authenticated;
GRANT EXECUTE ON FUNCTION search_messages(TEXT, VARCHAR, VARCHAR, VARCHAR, TIMESTAMPTZ, TIMESTAMPTZ, INTEGER, INTEGER) TO authenticated;
-- Comments for documentation
COMMENT ON TABLE whatsapp_conversations IS 'Stores WhatsApp conversation metadata';
COMMENT ON TABLE whatsapp_messages IS 'Stores individual WhatsApp messages with full content and metadata';
COMMENT ON COLUMN whatsapp_messages.whatsapp_message_id IS 'Original message ID from WhatsApp API';
COMMENT ON COLUMN whatsapp_messages.reply_to_message_id IS 'References the parent message for threaded conversations';
COMMENT ON COLUMN whatsapp_messages.whatsapp_reply_to_id IS 'Original WhatsApp message ID being replied to';
COMMENT ON COLUMN whatsapp_messages.content IS 'Message content in JSON format - text, media info, location, etc.';
COMMENT ON COLUMN whatsapp_messages.metadata IS 'Additional metadata like contact info, business context, etc.';
COMMENT ON FUNCTION update_conversation_on_message() IS 'Automatically maintains conversation metadata when messages are inserted';
COMMENT ON FUNCTION cleanup_old_messages(INTEGER) IS 'Removes messages older than specified days and updates conversation counts';
COMMENT ON FUNCTION get_conversation_stats(VARCHAR) IS 'Returns comprehensive statistics for a specific conversation';
COMMENT ON FUNCTION search_messages IS 'Full-text search across messages with multiple filter options';
-- Success message
DO $$
BEGIN
RAISE NOTICE 'WhatsApp SDK schema created successfully!';
RAISE NOTICE 'Tables: whatsapp_conversations, whatsapp_messages';
RAISE NOTICE 'Functions: update_conversation_on_message, mark_conversation_read, cleanup_old_messages, get_conversation_stats, search_messages';
RAISE NOTICE 'Views: message_threads, conversation_summary, message_analytics';
RAISE NOTICE 'Remember to uncomment RLS policies if you need row-level security!';
END $$;
Copy
✅ WhatsApp SDK schema created successfully!
1.3 Verify Tables Created
Run this query to verify:Copy
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name LIKE 'whatsapp_%';
whatsapp_messageswhatsapp_conversations
1.4 Get API Credentials
- Go to Settings → API
- Copy these values:
- Project URL:
https://xxxxx.supabase.co - Service Role Key (⚠️ NOT the anon/public key)
- Project URL:
IMPORTANT: Use the Service Role Key, not the anon/public key. The service role key bypasses Row Level Security (RLS) and is only safe for backend/API routes.
Step 2: Install Dependencies
Copy
npm install whatsapp-client-sdk@latest @supabase/supabase-js
Copy
yarn add whatsapp-client-sdk@latest @supabase/supabase-js
Step 3: Configure Environment Variables
Create a.env.local file (or .env for non-Next.js projects):
Copy
# WhatsApp Credentials
WHATSAPP_ACCESS_TOKEN=your_whatsapp_access_token
WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id
WHATSAPP_WEBHOOK_TOKEN=your_webhook_verify_token
# Supabase Credentials
SUPABASE_URL=https://xxxxx.supabase.co
SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # ⚠️ Use Service Role Key
Step 4: Initialize SDK with Storage
Basic Setup
Copy
import { WhatsAppClient } from 'whatsapp-client-sdk';
const client = new WhatsAppClient({
accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
webhookVerifyToken: process.env.WHATSAPP_WEBHOOK_TOKEN!,
// Storage configuration
storage: {
enabled: true,
provider: 'supabase',
options: {
url: process.env.SUPABASE_URL!,
apiKey: process.env.SUPABASE_KEY!,
schema: 'public', // Optional, default: 'public'
tablePrefix: 'whatsapp_' // Optional, default: 'whatsapp_'
},
features: {
persistIncoming: true, // Save incoming messages
persistOutgoing: true, // Save outgoing messages
persistStatus: true, // Update message status (sent, delivered, read)
autoConversations: true, // Auto-create conversations
createThreads: true, // Track message threads/replies
enableSearch: true, // Enable full-text search
retentionDays: 90 // Auto-delete messages after 90 days
}
}
});
// Initialize storage
await client.initializeStorage();
Verify Storage is Enabled
Copy
if (client.isStorageEnabled()) {
console.log('✅ Storage is enabled and ready');
} else {
console.error('❌ Storage failed to initialize');
}
Step 5: Setup Webhooks with Storage
Next.js App Router Example
Createapp/api/webhook/route.ts:
Copy
import { WhatsAppClient } from 'whatsapp-client-sdk';
import { NextRequest, NextResponse } from 'next/server';
const client = new WhatsAppClient({
accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
webhookVerifyToken: process.env.WHATSAPP_WEBHOOK_TOKEN!,
storage: {
enabled: true,
provider: 'supabase',
options: {
url: process.env.SUPABASE_URL!,
apiKey: process.env.SUPABASE_KEY!
},
features: {
persistIncoming: true,
persistOutgoing: true,
persistStatus: true,
autoConversations: true,
createThreads: true,
enableSearch: true
}
}
});
// Initialize storage once
let initialized = false;
const init = async () => {
if (!initialized) {
await client.initializeStorage();
initialized = true;
}
};
// Create webhook processor
const webhookProcessor = client.createWebhookProcessor({
enableBuffer: true,
bufferTimeMs: 5000,
onTextMessage: async (messages) => {
const messagesArray = Array.isArray(messages) ? messages : [messages];
for (const message of messagesArray) {
// Messages are automatically saved to Supabase
// Just handle your business logic here
const text = message.text?.toLowerCase() || '';
if (text.includes('hello') || text.includes('hola')) {
await client.sendText(message.from, '👋 Hi! How can I help you?');
} else {
await client.sendText(message.from, `You said: "${message.text}"`);
}
await client.markMessageAsRead(message.id);
}
},
onMessageStatusUpdate: async (status) => {
// Status updates are automatically saved to Supabase
console.log(`Message ${status.id} is now ${status.status}`);
},
onError: async (error) => {
console.error('Webhook error:', error);
}
});
// GET endpoint for webhook verification
export async function GET(request: NextRequest) {
try {
await init();
const searchParams = request.nextUrl.searchParams;
const result = await webhookProcessor.processWebhook(
{} as any,
Object.fromEntries(searchParams)
);
return new NextResponse(String(result.response), {
status: result.status
});
} catch (error) {
console.error('GET webhook error:', error);
return new NextResponse('Internal Server Error', { status: 500 });
}
}
// POST endpoint for receiving messages
export async function POST(request: NextRequest) {
try {
await init();
const body = await request.json();
const result = await webhookProcessor.processWebhook(body, {});
return new NextResponse('OK', { status: result.status });
} catch (error) {
console.error('POST webhook error:', error);
return new NextResponse('Internal Server Error', { status: 500 });
}
}
Express.js Example
Copy
import express from 'express';
import { WhatsAppClient } from 'whatsapp-client-sdk';
const app = express();
app.use(express.json());
const client = new WhatsAppClient({
accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
webhookVerifyToken: process.env.WHATSAPP_WEBHOOK_TOKEN!,
storage: {
enabled: true,
provider: 'supabase',
options: {
url: process.env.SUPABASE_URL!,
apiKey: process.env.SUPABASE_KEY!
},
features: {
persistIncoming: true,
persistOutgoing: true,
persistStatus: true,
autoConversations: true,
createThreads: true,
enableSearch: true
}
}
});
// Initialize storage
await client.initializeStorage();
const webhookProcessor = client.createWebhookProcessor({
enableBuffer: true,
onTextMessage: async (message) => {
await client.sendText(message.from, `You said: "${message.text}"`);
}
});
// Webhook verification
app.get('/webhook', (req, res) => {
const mode = req.query['hub.mode'] as string;
const token = req.query['hub.verify_token'] as string;
const challenge = req.query['hub.challenge'] as string;
const result = client.verifyWebhook(mode, token, challenge);
if (result !== null) {
res.status(200).send(result);
} else {
res.status(403).send('Forbidden');
}
});
// Receive messages
app.post('/webhook', async (req, res) => {
try {
const result = await webhookProcessor.processWebhook(req.body, req.query);
res.status(result.status).send(result.response);
} catch (error) {
console.error('Webhook error:', error);
res.status(500).send('Error');
}
});
app.listen(3000, () => {
console.log('✅ Server running on port 3000');
});
Querying Stored Messages
See the Querying Data guide for detailed examples of:- Getting conversation history
- Searching messages
- Tracking message threads
- Analyzing conversations
- Exporting data
Copy
// Get conversation history
const conversation = await client.getConversation('+1234567890', {
limit: 50,
offset: 0
});
console.log(`Total messages: ${conversation.totalMessages}`);
conversation.messages.forEach(msg => {
console.log(`[${msg.direction}] ${msg.content.text || 'Media'}`);
});
Troubleshooting
Storage Not Enabling
Problem:isStorageEnabled() returns false
Solutions:
-
Verify you’re using Service Role Key, not Anon Key
Copy
# Check your .env file # Service Role Key contains "service_role" when decoded echo $SUPABASE_KEY | cut -d. -f2 | base64 -d # Should show: "role":"service_role" -
Check tables exist
Copy
SELECT table_name FROM information_schema.tables WHERE table_name LIKE 'whatsapp_%'; -
Check initialization logs
Copy
try { await client.initializeStorage(); console.log('Storage enabled:', client.isStorageEnabled()); } catch (error) { console.error('Initialization error:', error); }
Permission Errors
Problem:insert or update violates foreign key constraint
Solution: Make sure autoConversations: true is enabled. This automatically creates conversations before saving messages.
Connection Errors
Problem:Failed to connect to Supabase
Solutions:
- Verify URL is correct (should start with
https://) - Check API key is valid
- Test connection manually:
Copy
import { createClient } from '@supabase/supabase-js'; const supabase = createClient( process.env.SUPABASE_URL!, process.env.SUPABASE_KEY! ); const { data, error } = await supabase .from('whatsapp_messages') .select('id') .limit(1); if (error) console.error('Connection error:', error); else console.log('✅ Connected successfully');
RLS (Row Level Security) Issues
If you get permission denied errors with Service Role Key:Copy
-- Disable RLS (for testing only)
ALTER TABLE whatsapp_messages DISABLE ROW LEVEL SECURITY;
ALTER TABLE whatsapp_conversations DISABLE ROW LEVEL SECURITY;
For production, keep RLS enabled and use Service Role Key only in backend.
Best Practices
1. Security
- ✅ Use Service Role Key only in backend/API routes
- ✅ Never expose Service Role Key in frontend code
- ✅ Use environment variables for all credentials
- ✅ Add
.envto.gitignore
2. Performance
- ✅ Enable
enableBuffer: truein webhook processor - ✅ Set appropriate
bufferTimeMs(default: 5000ms) - ✅ Use
retentionDaysto auto-cleanup old data - ✅ Create indexes on frequently queried fields
3. Error Handling
Copy
const webhookProcessor = client.createWebhookProcessor({
onTextMessage: async (message) => {
try {
// Your logic here
} catch (error) {
console.error('Message handling error:', error);
// Don't throw - let other messages process
}
},
onError: async (error, message) => {
// Log critical errors
console.error('Critical webhook error:', error);
// Send alert to monitoring service
}
});
4. Cleanup Resources
Copy
// When shutting down your application
process.on('SIGTERM', async () => {
webhookProcessor.destroy(); // Clean up timers
await client.disconnectStorage(); // Close Supabase connection
process.exit(0);
});
Schema Overview
whatsapp_messages Table
| Column | Type | Description |
|---|---|---|
| id | UUID | Primary key |
| whatsapp_message_id | VARCHAR | WhatsApp’s message ID |
| conversation_id | UUID | Foreign key to conversations |
| phone_number_id | VARCHAR | Your business phone ID |
| from_phone | VARCHAR | Sender phone number |
| to_phone | VARCHAR | Recipient phone number |
| message_type | VARCHAR | text, image, video, etc. |
| content | JSONB | Message content (text, media, etc.) |
| reply_to_message_id | UUID | Parent message for threads |
| timestamp | TIMESTAMPTZ | Message timestamp |
| status | VARCHAR | sent, delivered, read, failed |
| direction | VARCHAR | incoming or outgoing |
| metadata | JSONB | Additional metadata |
whatsapp_conversations Table
| Column | Type | Description |
|---|---|---|
| id | UUID | Primary key |
| phone_number | VARCHAR | Customer phone number |
| business_phone_id | VARCHAR | Your business phone ID |
| last_message_at | TIMESTAMPTZ | Last message timestamp |
| message_count | INTEGER | Total message count |
| unread_count | INTEGER | Unread message count |
| metadata | JSONB | Additional metadata |
Next Steps
Query Messages
Learn how to query and search stored messages
Best Practices
Security, performance, and production tips
Custom Adapters
Build custom storage adapters for other databases
Webhook System
Learn more about webhook processing