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 |