A fully functional, production-ready WhatsApp Business integration platform built with Next.js 15, Supabase, WhatsApp Cloud API, and AWS S3.
Features β’ Quick Start β’ Documentation β’ Deployment
- Overview
- Complete Feature List
- Technology Stack
- Quick Start
- Setup Guide
- Features Documentation
- API Reference
- Deployment
- Troubleshooting
- Contributing
WaChat is an enterprise-grade WhatsApp Business integration platform that enables businesses to manage customer conversations through a modern, intuitive web interface. It provides everything you need for professional WhatsApp messaging: real-time chat, media handling, template management, broadcast groups, and more.
β¨ Production Ready - Built for scale with enterprise-grade architecture
π Real-time Everything - Instant message delivery using WebSockets
π± Full WhatsApp Integration - Complete WhatsApp Cloud API support
π¨ Beautiful UI - WhatsApp-like interface with dark mode
π Secure by Default - Row-level security and encrypted storage
β‘ Lightning Fast - Optimized for performance
- β Bidirectional Chat - Send and receive messages instantly
- β Text Messages - Full support with emoji and formatting
- β Message Status - Read/unread status with timestamps
- β Unread Indicators - Visual badges and separators
- β Auto-scroll - Jump to unread messages automatically
- β Optimistic UI - Instant message display before server confirmation
- β Real-time Sync - WebSocket-based instant updates
- β Image Messages - JPG, PNG, WebP, GIF support with captions
- β Video Messages - MP4, MOV, AVI with native HTML5 player
- β Audio Messages - MP3, AAC, voice messages with waveform
- β Document Messages - PDF, DOC, XLS, PPT with download
- β Drag & Drop Upload - Intuitive file upload
- β Multi-file Upload - Send multiple files simultaneously
- β Media Preview - Preview before sending
- β Download Support - Download any media file
- β Create Broadcast Groups - Organize contacts into groups
- β Group Naming - Custom names for easy identification
- β Member Management - Add/remove members easily
- β Member Count - See group size at a glance
- β Edit Groups - Update group details anytime
- β Delete Groups - Remove groups when no longer needed
- β Group Search - Filter contacts by group names
- β Text Broadcasts - Send text to all members simultaneously
- β Template Broadcasts - Send template messages to groups
- β Personal Delivery - Each member receives as individual message
- β Broadcast History - View all broadcast messages in chat window
- β Real-time Broadcast - Messages appear instantly in broadcast window
- β Individual Tracking - See messages in each member's chat
- β Unread Counts - Per-member unread message tracking
- β Broadcast Status - Success/failure tracking for each recipient
- β Group Unread Badge - Shows total unread from all members
- β Member Unread Count - Individual unread count per member
- β Quick Navigation - Click member to open their individual chat
- β Latest Message Preview - See last broadcast in user list
- β Visual Builder - Create templates with real-time preview
- β Multi-language - 14+ languages (English, Spanish, French, German, Arabic, Hindi, Chinese, etc.)
- β Template Components - Header, Body, Footer, Buttons
- β Dynamic Variables - Use {{1}}, {{2}}, etc. for personalization
- β Button Types - Quick Reply, URL, Phone Number, Catalog
- β Media Headers - Image, video, document headers
- β Rich Formatting - Bold, italic, emojis support
- β Template Library - Browse and search all templates
- β Template Categories - Marketing, Utility, Authentication
- β Status Tracking - Monitor approval status (Pending, Approved, Rejected)
- β Template Sending - Send from chat with variable filling
- β Broadcast Templates - Send templates to broadcast groups
- β Template Preview - See how it looks before sending
- β Template Deletion - Remove unwanted templates
- β Variable Validation - Ensure all variables are filled
- β Persistent Storage - All media stored permanently in S3
- β Pre-signed URLs - Secure, time-limited access (24-hour expiry)
- β Automatic Refresh - Expired URLs refresh automatically
- β Organized Structure - Media organized by sender
- β Encryption - Data encrypted at rest
- β HTTPS Only - Secure access only
- β Smart Caching - Efficient media loading
- β Image Optimization - Next.js automatic optimization
- β Lazy Loading - Load media on demand
- β Thumbnail Generation - Smaller previews for lists
- β Video Preload - Metadata only until play
- β Audio Management - Single audio plays at a time
- β Download Manager - Efficient file downloads
- β Custom Names - Set custom names for contacts
- β Name Hierarchy - Custom Name β WhatsApp Name β Phone Number
- β Inline Editing - Quick name editing with hover controls
- β User Info Dialog - Comprehensive contact information
- β New Chat Creation - Create chats with phone number validation
- β Last Active Tracking - Monitor user activity
- β Smart Sorting - Sort by unread and recent activity
- β Contact Search - Search names and phone numbers
- β Group Filtering - Filter contacts by broadcast groups
- β Real-time Filter - Instant search results
- β Fuzzy Search - Find contacts even with typos
- β WhatsApp-like Interface - Familiar chat bubble design
- β Theme Switcher - Light, Dark, System themes
- β Responsive Design - Mobile-first with desktop optimization
- β Smooth Animations - Fade-in, slide-up, scale effects
- β Loading States - Professional loading indicators
- β Error Handling - Graceful error messages
- β Touch Gestures - Mobile-optimized interactions
- β Keyboard Shortcuts - ESC to close dialogs
- β Auto-scroll - Smart scroll to unread or latest
- β Message Grouping - Group by date with separators
- β Typing Indicators - Show when typing (future)
- β Read Receipts - Visual read status
- β Time Formatting - Smart time display (Today, Yesterday, etc.)
- β Unread Separator - Red line showing unread messages
- β Badges - Green badges for unread counts
- β Supabase Auth - Secure user authentication
- β Email/Password - Traditional login
- β Password Reset - Forgot password flow
- β Protected Routes - Middleware-based protection
- β Session Management - Automatic session refresh
- β Secure Cookies - HttpOnly, Secure cookies
- β Row Level Security (RLS) - Database-level access control
- β User Isolation - Users can only see their data
- β SQL Injection Prevention - Parameterized queries
- β Function Security - SECURITY DEFINER functions
- β API Authentication - All routes require valid session
- β Input Validation - XSS prevention and sanitization
- β Phone Validation - E.164 format validation
- β File Type Validation - WhatsApp-supported types only
- β File Size Limits - Prevent oversized uploads
- β CORS Configuration - Restricted origins
- β Rate Limiting - Prevent abuse
- β Strategic Indexes - Optimized query performance
- β Database Views - Pre-computed complex queries
- β Database Functions - Atomic operations
- β Full Replication - Real-time enabled tables
- β Connection Pooling - Efficient connections
- β Smart Preloading - Load users first, then conversations
- β Parallel Processing - Multiple operations simultaneously
- β Debounced Updates - Prevent excessive re-renders
- β Code Splitting - Dynamic imports for heavy components
- β Memory Management - Proper cleanup and subscriptions
- β WebSocket Connections - Persistent connections
- β Channel Management - Unique channels per conversation
- β Duplicate Prevention - Smart message deduplication
- β Subscription Cleanup - Prevent memory leaks
- β Optimistic Updates - Instant UI feedback
Framework: Next.js 15 with App Router
UI Library: React 19
Language: TypeScript 5
Styling: Tailwind CSS 3
Icons: Lucide React
Components: Shadcn/ui
State: React Hooks
API: Next.js API Routes
Database: PostgreSQL (Supabase)
Real-time: Supabase Real-time (WebSocket)
Storage: AWS S3
Authentication: Supabase Auth
Functions: PostgreSQL Functions
WhatsApp: Meta WhatsApp Cloud API (v23.0)
Cloud Storage: AWS SDK v3
Image Optimize: Next.js Image Component
Package Manager: npm
Version Control: Git
Deployment: Vercel (recommended)
Before you begin, ensure you have:
- Node.js 18+ installed (Download)
- npm or yarn package manager
- Supabase Account (Sign up)
- Meta Business Account (Sign up)
- WhatsApp Business API access
- AWS Account for S3 storage (Sign up)
# Clone the repository
git clone <your-repo-url>
cd wachat
# Install dependencies
npm install
# Create environment file
cp .env.example .env.local
# Edit .env.local with your credentials
nano .env.local
# Run database migrations (see Setup Guide)
# Then start development server
npm run devVisit http://localhost:3000 - you're ready to go! π
- Go to database.new
- Create a new project
- Save your database password securely
- Note your Project URL and Anon Key
Execute the following SQL in Supabase SQL Editor:
-- ============================================
-- USERS TABLE
-- ============================================
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
custom_name TEXT DEFAULT NULL,
whatsapp_name TEXT DEFAULT NULL,
last_active TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- ============================================
-- MESSAGES TABLE
-- ============================================
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
sender_id TEXT NOT NULL,
receiver_id TEXT NOT NULL,
content TEXT NOT NULL,
timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_sent_by_me BOOLEAN DEFAULT FALSE,
message_type TEXT DEFAULT 'text',
media_data JSONB,
is_read BOOLEAN DEFAULT FALSE,
read_at TIMESTAMP WITH TIME ZONE,
FOREIGN KEY (sender_id) REFERENCES users(id),
FOREIGN KEY (receiver_id) REFERENCES users(id)
);
-- ============================================
-- BROADCAST GROUPS TABLES
-- ============================================
CREATE TABLE IF NOT EXISTS chat_groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS group_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
group_id UUID NOT NULL REFERENCES chat_groups(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
added_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(group_id, user_id)
);
-- ============================================
-- USER SETTINGS TABLE (Multi-Tenant Support)
-- ============================================
CREATE TABLE IF NOT EXISTS user_settings (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
access_token TEXT,
phone_number_id TEXT,
business_account_id TEXT,
verify_token TEXT,
webhook_token TEXT UNIQUE,
api_version TEXT DEFAULT 'v23.0',
webhook_verified BOOLEAN DEFAULT FALSE,
access_token_added BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- ============================================
-- INDEXES FOR PERFORMANCE
-- ============================================
CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_id);
CREATE INDEX IF NOT EXISTS idx_messages_receiver ON messages(receiver_id);
CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_messages_is_read ON messages(is_read);
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(sender_id, receiver_id, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_messages_media_data ON messages USING GIN (media_data);
CREATE INDEX IF NOT EXISTS idx_group_members_group_id ON group_members(group_id);
CREATE INDEX IF NOT EXISTS idx_group_members_user_id ON group_members(user_id);
CREATE INDEX IF NOT EXISTS idx_chat_groups_owner_id ON chat_groups(owner_id);
CREATE INDEX IF NOT EXISTS idx_user_settings_phone_number_id ON user_settings(phone_number_id);
CREATE INDEX IF NOT EXISTS idx_user_settings_webhook_token ON user_settings(webhook_token);
CREATE INDEX IF NOT EXISTS idx_user_settings_business_account_id ON user_settings(business_account_id);
-- ============================================
-- ENABLE REAL-TIME REPLICATION
-- ============================================
ALTER TABLE users REPLICA IDENTITY FULL;
ALTER TABLE messages REPLICA IDENTITY FULL;
ALTER TABLE chat_groups REPLICA IDENTITY FULL;
ALTER TABLE group_members REPLICA IDENTITY FULL;
-- Enable real-time for tables
ALTER PUBLICATION supabase_realtime ADD TABLE users;
ALTER PUBLICATION supabase_realtime ADD TABLE messages;
ALTER PUBLICATION supabase_realtime ADD TABLE chat_groups;
ALTER PUBLICATION supabase_realtime ADD TABLE group_members;
-- ============================================
-- ROW LEVEL SECURITY (RLS)
-- ============================================
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE chat_groups ENABLE ROW LEVEL SECURITY;
ALTER TABLE group_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_settings ENABLE ROW LEVEL SECURITY;
-- Users table policies
CREATE POLICY "Users can view all users" ON users
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Authenticated users can insert users" ON users
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Authenticated users can update users" ON users
FOR UPDATE USING (auth.role() = 'authenticated');
-- Messages table policies
CREATE POLICY "Users can view all messages" ON messages
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Users can send messages" ON messages
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Users can update messages" ON messages
FOR UPDATE USING (auth.role() = 'authenticated');
-- Broadcast groups policies
CREATE POLICY "Users can view their own groups" ON chat_groups
FOR SELECT USING (auth.uid() = owner_id);
CREATE POLICY "Users can create groups" ON chat_groups
FOR INSERT WITH CHECK (auth.uid() = owner_id);
CREATE POLICY "Users can update their own groups" ON chat_groups
FOR UPDATE USING (auth.uid() = owner_id);
CREATE POLICY "Users can delete their own groups" ON chat_groups
FOR DELETE USING (auth.uid() = owner_id);
-- Group members policies
CREATE POLICY "Users can view members of their groups" ON group_members
FOR SELECT USING (
EXISTS (
SELECT 1 FROM chat_groups
WHERE chat_groups.id = group_members.group_id
AND chat_groups.owner_id = auth.uid()
)
);
CREATE POLICY "Users can add members to their groups" ON group_members
FOR INSERT WITH CHECK (
EXISTS (
SELECT 1 FROM chat_groups
WHERE chat_groups.id = group_members.group_id
AND chat_groups.owner_id = auth.uid()
)
);
CREATE POLICY "Users can remove members from their groups" ON group_members
FOR DELETE USING (
EXISTS (
SELECT 1 FROM chat_groups
WHERE chat_groups.id = group_members.group_id
AND chat_groups.owner_id = auth.uid()
)
);
-- User settings policies
CREATE POLICY "Users can view own settings" ON user_settings
FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Users can insert own settings" ON user_settings
FOR INSERT WITH CHECK (auth.uid() = id);
CREATE POLICY "Users can update own settings" ON user_settings
FOR UPDATE USING (auth.uid() = id);
-- ============================================
-- DATABASE FUNCTIONS
-- ============================================
-- Function: Update custom names
CREATE OR REPLACE FUNCTION update_user_custom_name(user_id TEXT, new_custom_name TEXT)
RETURNS BOOLEAN AS $$
BEGIN
UPDATE users
SET custom_name = new_custom_name
WHERE id = user_id;
RETURN FOUND;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function: Mark messages as read
CREATE OR REPLACE FUNCTION mark_messages_as_read(current_user_id TEXT, other_user_id TEXT)
RETURNS INTEGER AS $$
DECLARE
affected_rows INTEGER;
BEGIN
UPDATE messages
SET is_read = TRUE, read_at = NOW()
WHERE receiver_id = current_user_id
AND sender_id = other_user_id
AND is_read = FALSE;
GET DIAGNOSTICS affected_rows = ROW_COUNT;
RETURN affected_rows;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function: Get conversation messages
CREATE OR REPLACE FUNCTION get_conversation_messages(other_user_id TEXT)
RETURNS TABLE (
id TEXT,
sender_id TEXT,
receiver_id TEXT,
content TEXT,
message_timestamp TIMESTAMP WITH TIME ZONE,
is_sent_by_me BOOLEAN,
message_type TEXT,
media_data JSONB,
is_read BOOLEAN,
read_at TIMESTAMP WITH TIME ZONE
) AS $$
BEGIN
RETURN QUERY
SELECT
m.id,
m.sender_id,
m.receiver_id,
m.content,
m.timestamp as message_timestamp,
(m.sender_id = (SELECT id FROM auth.users() LIMIT 1)) as is_sent_by_me,
m.message_type,
m.media_data,
m.is_read,
m.read_at
FROM messages m
WHERE (m.sender_id = other_user_id OR m.receiver_id = other_user_id)
ORDER BY m.timestamp ASC;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function: Get unread conversations
CREATE OR REPLACE FUNCTION get_unread_conversations(limit_count INTEGER DEFAULT 10)
RETURNS TABLE(
conversation_id TEXT,
display_name TEXT,
unread_count BIGINT,
last_message_time TIMESTAMP WITH TIME ZONE
) AS $$
BEGIN
RETURN QUERY
SELECT
m.sender_id as conversation_id,
COALESCE(u.custom_name, u.whatsapp_name, u.name, u.id) as display_name,
COUNT(*) as unread_count,
MAX(m.timestamp) as last_message_time
FROM messages m
LEFT JOIN users u ON u.id = m.sender_id
WHERE m.is_read = FALSE
GROUP BY m.sender_id, u.custom_name, u.whatsapp_name, u.name, u.id
ORDER BY last_message_time DESC
LIMIT limit_count;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function: Create or get user
CREATE OR REPLACE FUNCTION create_or_get_user(phone_number TEXT, user_name TEXT DEFAULT NULL)
RETURNS TABLE(
id TEXT,
name TEXT,
custom_name TEXT,
whatsapp_name TEXT,
last_active TIMESTAMP WITH TIME ZONE,
is_new BOOLEAN
) AS $$
DECLARE
user_exists BOOLEAN;
BEGIN
SELECT EXISTS(SELECT 1 FROM users WHERE users.id = phone_number) INTO user_exists;
IF NOT user_exists THEN
INSERT INTO users (id, name, whatsapp_name, last_active)
VALUES (phone_number, COALESCE(user_name, phone_number), user_name, NOW());
RETURN QUERY
SELECT users.id, users.name, users.custom_name, users.whatsapp_name, users.last_active, TRUE as is_new
FROM users
WHERE users.id = phone_number;
ELSE
IF user_name IS NOT NULL THEN
UPDATE users
SET whatsapp_name = user_name, last_active = NOW()
WHERE users.id = phone_number;
END IF;
RETURN QUERY
SELECT users.id, users.name, users.custom_name, users.whatsapp_name, users.last_active, FALSE as is_new
FROM users
WHERE users.id = phone_number;
END IF;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function: Get user groups with counts
CREATE OR REPLACE FUNCTION get_user_groups_with_counts()
RETURNS TABLE (
group_id UUID,
group_name TEXT,
group_description TEXT,
member_count BIGINT,
unread_count BIGINT,
created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE
) AS $$
BEGIN
RETURN QUERY
SELECT
cg.id AS group_id,
cg.name AS group_name,
cg.description AS group_description,
COUNT(DISTINCT gm.id) AS member_count,
COALESCE(SUM(
(SELECT COUNT(*)
FROM messages m
WHERE m.sender_id = gm.user_id
AND m.receiver_id = (SELECT id FROM auth.users() LIMIT 1)
AND m.is_read = false
)
), 0) AS unread_count,
cg.created_at,
cg.updated_at
FROM chat_groups cg
LEFT JOIN group_members gm ON gm.group_id = cg.id
WHERE cg.owner_id = auth.uid()
GROUP BY cg.id, cg.name, cg.description, cg.created_at, cg.updated_at
ORDER BY cg.updated_at DESC;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function: Get group members with details
CREATE OR REPLACE FUNCTION get_group_members_with_details(p_group_id UUID)
RETURNS TABLE (
member_id UUID,
user_id VARCHAR(255),
whatsapp_name TEXT,
custom_name TEXT,
added_at TIMESTAMP WITH TIME ZONE,
unread_count BIGINT
) AS $$
BEGIN
RETURN QUERY
SELECT
gm.id AS member_id,
gm.user_id,
COALESCE(u.whatsapp_name, u.name) AS whatsapp_name,
u.custom_name,
gm.added_at,
COALESCE(
(SELECT COUNT(*)
FROM messages m
WHERE m.sender_id = gm.user_id
AND m.receiver_id = (SELECT owner_id FROM chat_groups WHERE id = p_group_id)
AND m.is_read = false
), 0
) AS unread_count
FROM group_members gm
LEFT JOIN users u ON u.id = gm.user_id
WHERE gm.group_id = p_group_id
ORDER BY NULLIF(u.custom_name, '') NULLS LAST, COALESCE(u.whatsapp_name, u.name);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function: Get group unread count
CREATE OR REPLACE FUNCTION get_group_unread_count(p_group_id UUID)
RETURNS BIGINT AS $$
DECLARE
total_unread BIGINT;
BEGIN
SELECT COALESCE(SUM(
(SELECT COUNT(*)
FROM messages m
WHERE m.sender_id = gm.user_id
AND m.receiver_id = (SELECT owner_id FROM chat_groups WHERE id = p_group_id)
AND m.is_read = false
)
), 0)
INTO total_unread
FROM group_members gm
WHERE gm.group_id = p_group_id;
RETURN total_unread;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Trigger: Auto-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 update_chat_groups_updated_at
BEFORE UPDATE ON chat_groups
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_user_settings_updated_at
BEFORE UPDATE ON user_settings
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- ============================================
-- USER CONVERSATIONS VIEW
-- ============================================
CREATE OR REPLACE VIEW user_conversations AS
WITH unread_counts AS (
SELECT
sender_id,
COUNT(*) as unread_count
FROM messages
WHERE is_read = FALSE
GROUP BY sender_id
),
latest_messages AS (
SELECT DISTINCT ON (
CASE
WHEN sender_id < receiver_id THEN sender_id || '-' || receiver_id
ELSE receiver_id || '-' || sender_id
END
)
sender_id,
receiver_id,
content,
message_type,
timestamp as last_message_time,
sender_id as last_message_sender
FROM messages
ORDER BY
CASE
WHEN sender_id < receiver_id THEN sender_id || '-' || receiver_id
ELSE receiver_id || '-' || sender_id
END,
timestamp DESC
)
SELECT DISTINCT
u.id,
COALESCE(u.custom_name, u.whatsapp_name, u.name, u.id) as display_name,
u.custom_name,
u.whatsapp_name,
u.name as original_name,
u.last_active,
COALESCE(unread_counts.unread_count, 0) as unread_count,
lm.content as last_message,
lm.message_type as last_message_type,
lm.last_message_time,
lm.last_message_sender,
CASE WHEN unread_counts.unread_count > 0 THEN 1 ELSE 0 END as has_unread
FROM users u
LEFT JOIN unread_counts ON u.id = unread_counts.sender_id
LEFT JOIN latest_messages lm ON u.id = lm.sender_id OR u.id = lm.receiver_id
ORDER BY has_unread DESC, last_message_time DESC NULLS LAST;- Go to Database β Replication in Supabase dashboard
- Enable replication for:
users,messages,chat_groups,group_members
- Go to Meta Developers
- Click Create App β Choose Business type
- Add WhatsApp product to your app
You'll need these credentials from Meta Business Suite:
-
Access Token (Permanent Token)
- Go to Meta Developers β Your App β WhatsApp β API Setup
- Click Generate Access Token
- Make it permanent (not test token)
- Copy and save securely
-
Phone Number ID
- In the same API Setup page
- Under your test/production phone number
- Copy the numeric Phone Number ID
-
Business Account ID
- Go to Meta Business Suite β Settings β Business Info
- Copy your Business Account ID
- OR check the URL in Developers Console: it contains your Business Account ID
-
Verify Token (You Create This)
- Create a secure random string (e.g.,
my-secure-webhook-token-2024) - You'll use this when setting up the webhook
- Create a secure random string (e.g.,
π― NEW: User-Specific Configuration
Instead of using environment variables, each user configures their own WhatsApp credentials through the app:
-
After deployment, sign up / log in to your app
-
Navigate to Setup (
/protected/setup) - you'll be redirected automatically -
Fill in the Access Token Configuration form:
- β Access Token - Paste your permanent token from Meta
- β Phone Number ID - Paste your Phone Number ID
- β Business Account ID - Paste your Business Account ID
- β
API Version - Default:
v23.0(leave as is unless you need specific version)
-
Click Save Access Token
-
Fill in the Webhook Configuration form:
- β Verify Token - Enter your custom secure token
- β Copy the automatically generated Webhook URL (unique to you)
-
Click Save Webhook Configuration
Benefits of this approach:
- β¨ Multi-tenant: Multiple users can use different WhatsApp Business accounts
- π Secure: Each user's credentials are isolated in the database
- π Easy: No need to redeploy when changing credentials
- π₯ Scalable: Support multiple businesses from one deployment
- Go to Meta Developers β Your App β WhatsApp β Configuration
- Click Edit on Webhook
- Callback URL: Use the unique webhook URL from your setup page
- Format:
https://your-domain.com/api/webhook/[your-unique-token]
- Format:
- Verify Token: Enter the verify token you created in step 2.3
- Subscribe to messages field
- Click Verify and Save
- Return to your app - webhook should show as "β Verified"
aws s3 mb s3://wachat-media-bucket --region us-east-1Or via AWS Console:
- Go to AWS S3 Console
- Create bucket with unique name
- Block all public access β
- Create bucket
Create user with this policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::wachat-media-bucket/*"
}
]
}Save Access Key ID and Secret Access Key.
Create .env.local in project root:
# ============================================
# SUPABASE CONFIGURATION
# ============================================
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY=your_supabase_anon_key
# Supabase Service Role Key (CRITICAL for webhooks)
# This bypasses Row Level Security for webhook operations
# Get it from: Supabase Dashboard β Settings β API β service_role key
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
# ============================================
# AWS S3 CONFIGURATION
# ============================================
AWS_ACCESS_KEY_ID=your_aws_access_key_id
AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key
AWS_REGION=us-east-1
AWS_BUCKET_NAME=wachat-media-bucket
# ============================================
# WHATSAPP CONFIGURATION (Optional - Legacy)
# ============================================
# NOTE: WhatsApp credentials are now configured per-user through the UI
# You can optionally keep these for backward compatibility, but they're not required
# The app will use user-specific credentials from the databaseπ Important: The SUPABASE_SERVICE_ROLE_KEY is required for webhooks to work. This key allows the webhook endpoint to bypass Row Level Security (RLS) since webhook requests come from WhatsApp (external source) without user authentication.
Where to find it:
- Go to Supabase Dashboard
- Click Settings β API
- Under Project API keys, copy the
service_rolesecret key β οΈ Never expose this key to client-side code - it has admin privileges!
# Development mode
npm run dev
# Production build
npm run build
npm startVisit http://localhost:3000 π
First Time Setup:
After deploying the application and creating your account, you'll be automatically redirected to the setup page (/protected/setup).
Setup Page Features:
-
Access Token Configuration (Required for sending messages)
- π Access Token - Your permanent WhatsApp Business API token
- π± Phone Number ID - Your WhatsApp Business phone number ID
- π’ Business Account ID - For template management
- π’ API Version - WhatsApp API version (default: v23.0)
-
Webhook Configuration (Required for receiving messages)
- π Verify Token - Your custom security token
- π Webhook URL - Automatically generated unique URL for you
Multi-User Support:
- Each user gets their own WhatsApp Business configuration
- Users can manage different businesses independently
- Credentials are securely stored in the database
- No need to redeploy when changing credentials
- Full data isolation between users
After Setup:
- β Access token configured β You can send messages and templates
- β Webhook configured & verified β You can receive messages
- π Both configured β Full bidirectional chat functionality
WaChat uses Supabase real-time subscriptions for instant message delivery:
How it works:
- WebSocket connections for instant updates
- Unique channels per conversation
- Sub-second message delivery
- Smart duplicate prevention
- Optimistic UI updates
Creating a Broadcast Group:
- Click Users icon in chat header
- Click Create broadcast group
- Enter group name and description
- Select members from contact list
- Click Create Group
Sending Broadcasts:
- Click Broadcast on a group
- Type message or select template
- Click Send
- Message delivered to all members individually
Key Benefits:
- Each member receives as personal message
- Track individual read status
- See messages in each member's chat
- Real-time broadcast window
Creating Templates:
- Navigate to Templates
- Click Create Template
- Fill template details:
- Name (lowercase, underscores only)
- Category (MARKETING, UTILITY, AUTHENTICATION)
- Language
- Add components:
- Header (optional): Text or media
- Body (required): Main message with variables
- Footer (optional): Small text
- Buttons (optional): Quick Reply, URL, Phone
- Use
{{1}},{{2}}for dynamic content - Submit for Meta approval
Sending Templates:
- Click template icon (π¬) in chat
- Select approved template
- Fill variable values
- Preview and send
Supported Types:
- Images: JPG, PNG, WebP, GIF (max 5MB)
- Videos: MP4, MOV, AVI (max 16MB)
- Audio: MP3, AAC, voice messages (max 16MB)
- Documents: PDF, DOC, XLS, PPT (max 100MB)
Upload Methods:
- Drag & drop files into chat window
- Click attachment icon (π)
- Multi-file selection supported
Custom Names:
Display Priority:
- Custom Name (user-set) β
- WhatsApp Name (from profile)
- Phone Number (fallback)
Edit Methods:
- Hover over user β Click edit icon
- Click chat header β User info dialog β Edit name
Create New Chat:
- Click + button
- Enter phone number:
+1234567890(E.164 format) - Optional: Add custom name
- Click Create Chat
All API routes require authentication via Supabase session.
Send text message.
Request:
{
"to": "+1234567890",
"message": "Hello!"
}
Response:
{
"success": true,
"messageId": "wamid.123..."
}Upload and send media.
FormData:
to: string
files: File[]
captions: string[]
Response:
{
"success": true,
"successCount": 2,
"failureCount": 0
}Send template message.
Request:
{
"to": "+1234567890",
"templateName": "order_confirmation",
"templateData": { ... },
"variables": {
"header": { "1": "John" },
"body": { "1": "12345" }
}
}Create broadcast group.
Request:
{
"name": "Marketing Team",
"description": "All marketing contacts",
"memberIds": ["+1234567890", "+9876543210"]
}Get all user's broadcast groups.
Send broadcast message.
Request:
{
"message": "Hello team!",
"messageType": "text"
}Fetch all templates.
Create new template.
Delete template.
Update custom name.
Create new chat.
Step-by-step:
-
Push to GitHub
git init git add . git commit -m "Initial commit" git push origin main
-
Deploy on Vercel
- Go to vercel.com
- Import GitHub repository
- Add environment variables
- Deploy!
-
Update Webhook
- Update WhatsApp webhook URL to:
https://your-app.vercel.app/api/webhook/YOUR_TOKEN
Required for Deployment:
- β
NEXT_PUBLIC_SUPABASE_URL - β
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY - β
SUPABASE_SERVICE_ROLE_KEYπ Critical for webhooks - β
AWS_ACCESS_KEY_ID - β
AWS_SECRET_ACCESS_KEY - β
AWS_REGION - β
AWS_BUCKET_NAME
Not Required (Configured Through UI):
- β
- Now set per-user inWHATSAPP_TOKEN/protected/setup - β
- Now set per-user inWHATSAPP_PHONE_NUMBER_ID/protected/setup - β
- Now set per-user inWHATSAPP_BUSINESS_ACCOUNT_ID/protected/setup - β
- Now set per-user inWHATSAPP_API_VERSION/protected/setup - β
- Now set per-user inWHATSAPP_VERIFY_TOKEN/protected/setup
Problem: Webhook receives messages but can't find user settings in database.
Root Cause: Row Level Security (RLS) blocking webhook queries.
Solution:
- Check
SUPABASE_SERVICE_ROLE_KEYis set in environment variables - Get it from: Supabase Dashboard β Settings β API β
service_rolekey - Add to
.env.localor deployment environment variables - Restart your application after adding the key
Why this happens:
- Webhooks come from WhatsApp (external source, no user auth)
- Regular Supabase client requires authentication
- RLS policies block unauthenticated queries
- Service role key bypasses RLS for webhook operations
Solution:
- Verify webhook URL is publicly accessible (test in browser)
- Check verify token matches between app and Meta settings
- Confirm subscribed to "messages" field in Meta webhook settings
- Check
SUPABASE_SERVICE_ROLE_KEYis set (see above) - Review webhook logs in your deployment platform
- Test with Meta's webhook test button
Solution:
- Complete Setup: Go to
/protected/setupand configure credentials - Verify access token is permanent (not test token - expires in 24h)
- Check phone number ID is correct (numeric ID from Meta)
- Check business account ID is correct
- Ensure recipient has WhatsApp account
- Review API version compatibility (default v23.0 works)
Solution:
- Enable real-time in Supabase dashboard
- Check replication is enabled for tables
- Verify WebSocket connections in browser console
- Review Supabase real-time logs
Solution:
- Verify S3 bucket configuration
- Check
next.config.tshas S3 hostname inremotePatterns - Confirm pre-signed URLs not expired (24-hour expiry)
- Test bucket permissions with IAM user
- Check AWS credentials are correct in environment variables
Solution:
- Complete Setup: Ensure Business Account ID is configured in
/protected/setup - Check business account ID matches your Meta Business Suite
- Verify access token has template permissions
- Check templates exist in Meta Business Manager
- Review template status (must be APPROVED to send)
Solution:
- You need to complete setup first
- Navigate to
/protected/setup - Configure at least one: Access Token OR Webhook
- After saving, you'll be able to access the chat interface
We welcome contributions! Here's how:
- Check existing issues
- Create detailed bug report
- Include reproduction steps
- Add screenshots
- Specify environment
- Fork repository
- Create feature branch
- Make changes
- Add tests
- Update documentation
- Submit pull request
- Follow TypeScript best practices
- Use ESLint configuration
- Write meaningful commits
- Add comments for complex logic
- Update README when needed
This project is licensed under the MIT License.
- Next.js - React Framework
- Supabase - Backend Platform
- Tailwind CSS - Styling
- Lucide Icons - Icons
- Shadcn/ui - Components
- Documentation: Read this README
- Issues: Check existing issues
- Discussions: GitHub Discussions
- Email: wachat@aryanshinde.in
WaChat is production-ready and waiting for your customers.
Start messaging now! π¬β¨
Built with β€οΈ using Next.js, Supabase, and WhatsApp Cloud API
Get Started β’ View Features β’ Read Docs