WhatsApp Mirror — Full-Stack Real-Time Bridge
A production-grade, highly-responsive WhatsApp Mirror application. This system allows you to remotely view and manage a WhatsApp account on a dark-themed Flutter mobile app (iOS + Android) by bridging commands and real-time events over a standalone WebSockets Node.js bridge server.
🏗️ Architecture
📱 Mobile Client (Flutter) ⚡ [WebSockets Protocol] ⚡ 🖥️ standalone Backend Server (Node.js) ⚙️ [Puppeteer] ⚙️ 🟩 WhatsApp Web (LocalAuth Session)
📂 Project Structure
whatsapp-app/
├── README.md ← Full system configuration & documentation
├── whatsapp_bridge/ ← standalone Node.js server
│ ├── package.json ← Package dependencies (whatsapp-web.js, ws, qrcode, express)
│ ├── server.js ← Bridge server entrypoint (Port 3025)
│ └── .wwebjs_auth/ ← Session persistence auth cache (git-ignored)
└── whatsapp_app/ ← Dark-themed Flutter Client
├── pubspec.yaml ← Flutter configuration and plugins
└── lib/
├── main.dart ← Flutter app entry point (GetX service initializer)
├── config/
│ └── app_config.dart ← Server host & port variables
├── theme/
│ └── app_theme.dart ← WhatsApp-style immersive Dark Theme
├── services/
│ └── whatsapp_service.dart ← Websocket request/response manager
├── models/
│ ├── conversation_model.dart
│ └── message_model.dart
├── controllers/
│ ├── conversations_controller.dart
│ └── chat_controller.dart
├── screens/
│ ├── conversations_screen.dart
│ ├── chat_screen.dart
│ └── qr_screen.dart
└── widgets/
├── conversation_tile.dart
└── message_bubble.dart
⚡ WebSockets Protocol Specification
To handle asynchronous requests and map them back to specific UI components or triggers, a strict Request-Response ID matching mechanism is implemented.
1. Client-to-Server Requests
Every request from the Flutter client MUST contain a unique requestId. The server replies with the exact same requestId.
A. Ping Check (ping)
- Request:
{ "type": "ping", "requestId": "1" } - Response:
{ "type": "pong", "ready": true, "requestId": "1" }
B. Get Conversation List (get_conversations)
- Request:
{ "type": "get_conversations", "limit": 50, "offset": 0, "requestId": "2" } - Response:
{ "type": "conversations", "data": [ { "id": "1234567890@c.us", "name": "Jane Doe", "isGroup": false, "unreadCount": 2, "avatar": "https://pps.whatsapp.net/v/...", "lastMessage": { "body": "Hello there!", "timestamp": 1716035000, "fromMe": false, "hasMedia": false }, "timestamp": 1716035000, "pinned": false, "isMuted": false } ], "total": 1, "requestId": "2" }
C. Get Message History (get_messages)
- Request:
{ "type": "get_messages", "chatId": "1234567890@c.us", "limit": 50, "requestId": "3" } - Response:
{ "type": "messages", "chatId": "1234567890@c.us", "data": [ { "id": "true_1234567890@c.us_ABC123", "body": "Hello there!", "fromMe": false, "timestamp": 1716035000, "type": "chat", "hasMedia": false, "isForwarded": false, "author": null, "ack": 4 } ], "requestId": "3" }
D. Send Message (send_message)
- Request:
{ "type": "send_message", "chatId": "1234567890@c.us", "text": "Hello!", "requestId": "4" } - Response:
{ "type": "message_sent", "chatId": "1234567890@c.us", "data": { "id": "true_1234567890@c.us_XYZ987", "body": "Hello!", "fromMe": true, "timestamp": 1716035005, "type": "chat", "hasMedia": false, "isForwarded": false, "author": null, "ack": 1 }, "requestId": "4" }
E. Mark Chat as Read (mark_read)
- Request:
{ "type": "mark_read", "chatId": "1234567890@c.us", "requestId": "5" } - Response:
{ "type": "marked_read", "chatId": "1234567890@c.us", "requestId": "5" }
F. Search Conversations (search_conversations)
- Request:
{ "type": "search_conversations", "query": "Jane", "requestId": "6" } - Response:
{ "type": "conversations", "data": [...], "search": true, "requestId": "6" }
2. Server-to-Client Push Events
The server broadcasts live events to all connected clients immediately.
- QR Code Broadcast:
{ "type": "qr", "qr": "data:image/png;base64,iVBORw0KGgo..." } - Authenticated:
{ "type": "authenticated" } - Client Ready:
{ "type": "ready" } - Status Updates:
{ "type": "status", "ready": true } - Client Disconnected:
{ "type": "disconnected", "reason": "Session expired or logged out" } - New Incoming Message:
{ "type": "new_message", "chatId": "1234567890@c.us", "data": { "id": "false_1234567890@c.us_DEF456", "body": "Live incoming text!", "fromMe": false, "timestamp": 1716035100, "type": "chat", "hasMedia": false, "isForwarded": false, "author": null, "ack": 2 } } - Message Delivery / Read Receipt (
message_ack):(Ack codes: 0 = Error/None, 1 = Pending, 2 = Sent, 3 = Delivered, 4 = Read){ "type": "message_ack", "messageId": "true_1234567890@c.us_XYZ987", "chatId": "1234567890@c.us", "ack": 4 }
🚀 Quick Setup & Installation
1. Server Setup (whatsapp_bridge/)
Navigate into the backend project, install dependencies, and start the standalone service:
cd whatsapp_bridge
npm install
node server.js
The server will boot up and bind to Port 3025. It will automatically print:
[SERVER] Standalone WhatsApp Bridge running on port 3025
2. Flutter App Setup (whatsapp_app/)
First, ensure that you have created the Flutter project structure using standard platform templates:
# Run this inside the workspace directory
flutter create whatsapp_app
Then, copy all files in whatsapp_app/ into the newly created project folder. Open the folder and install Dart packages:
cd whatsapp_app
flutter pub get
Run on Simulator / Device
flutter run
🌐 Production Deployment (CloudPanel Server)
Since this backend server acts as a standalone daemon and does not conflict with existing apps, it operates exclusively on Port 3025.
1. Create Node.js Site in CloudPanel
- Navigate to CloudPanel Admin Portal -> Sites -> Add Site -> Create a Node.js Application.
- Set Domain Name:
mywhatsappapp.interlap.com(or your subdomain). - Set Port:
3025. - Set Entry Point:
server.js. - Select Node.js Version:
18+ LTS(or Node 20 LTS).
2. Bypass Nginx Reverse Proxy
By default, CloudPanel routes external traffic from Port 80/443 through an Nginx reverse proxy. For our standalone WebSockets configuration, we want direct access. Make sure to open Port 3025 on your firewall (e.g. AWS Security Group or UFW):
sudo ufw allow 3025/tcp
This isolates Node.js directly on port 3025.
3. Keep Server Alive with PM2
We highly recommend running your Node.js application inside PM2 to ensure it auto-reconnects, logs diagnostic crashes, and recovers seamlessly:
# Install PM2 globally
npm install -g pm2
# Start the bridge server
pm2 start server.js --name "whatsapp-bridge"
# Persist server launch on reboot
pm2 save
pm2 startup
🔒 Security & Performance Features
- Session Persistence: Configured using
whatsapp-web.js's built-inLocalAuthsession strategies, meaning that once you scan the QR code once, you will not have to scan it again unless explicitly logged out. - Resilience & Crash Prevention: The server wraps critical events in structured
try/catchclauses and binds events toprocess.on('uncaughtException'), protecting the server from unexpected Puppeteer execution crashes. - Auto-Reconnection: If WhatsApp Web loses connection or the user logs out, the server attempts re-initialization automatically after a 5-second backoff delay.
- Dark Mode Design: Beautiful custom-tailored dark theme matching official WhatsApp guidelines (
#111B21,#1F2C34,#00A884) with full custom ripple alerts, status badges, and tick indicators.