A centralized, high-performance WhatsApp Gateway (SaaS-lite) designed to manage multiple WhatsApp sessions via a single dashboard.
Built with a Module-Driven Architecture (Vertical Slicing) on Fastify, ensuring rapid development without sacrificing performance. It solves common connection issues using Redis-based queuing and database-persistent sessions.
This project uses Baileys, an unofficial WhatsApp library. Baileys is not affiliated with WhatsApp Inc. and operates through reverse-engineering WhatsApp's web client. Please be aware of the following:
- Use at your own risk: WhatsApp's Terms of Service may not permit automated access via unofficial libraries.
- Account suspension risk: Using this library may result in your WhatsApp account being temporarily or permanently banned.
- No warranty: This project and Baileys are provided as-is without any warranty or official support from WhatsApp.
For official WhatsApp integration, consider using the WhatsApp Business API.
This project is specifically designed to manage multi-device WhatsApp logins using Baileys library, enabling you to:
- Create and manage multiple independent WhatsApp sessions (one per device/phone number)
- Automate WhatsApp messaging at scale via a centralized API
- Persist session credentials securely in PostgreSQL
- Handle high message throughput with Redis-based job queuing
- Monitor session status and message logs through a unified dashboard
- Multi-Tenant Support: Manage multiple independent WhatsApp sessions (Tenants) in one instance.
- Robust Queue System: Uses BullMQ (Redis) to handle high-throughput messaging and prevent rate-limiting bans.
- Persistent Sessions: Baileys credentials are stored in PostgreSQL, making the system container-friendly (stateless application logic).
- Real-time UI: Server-driven dashboard using EJS + HTMX for QR code streaming and status updates.
| Component | Technology | Reason |
|---|---|---|
| Runtime | Node.js (ESM) + TypeScript | Type safety & modern ecosystem. |
| Framework | Fastify | Low overhead, high performance. |
| Architecture | Module Driven (Vertical Slice) | Scalability & organized codebase. |
| Database | PostgreSQL + Drizzle ORM | Relational integrity & rapid query building. |
| Queue | Redis + BullMQ | Reliable job processing. |
| WhatsApp Engine | @whiskeysockets/baileys | Native WebSocket handling. |
| Frontend | EJS + HTMX + TailwindCSS | Zero-build step, server-driven UI. |
| Validation | TypeBox | JSON Schema validation & TS inference. |
src/
├── app.ts # App Factory & Autoload configuration
├── server.ts # Entry point
├── common/ # Shared utilities & Database Schema
├── plugins/ # Global setup (DB, Redis, Env, View)
└── modules/ # Feature-based Modules
├── whatsapp/ # WA Engine, Baileys Logic, Workers
└── dashboard/ # UI Controllers & Views
- Node.js >= 20
- Docker & Docker Compose
- npm
- Clone the repository
git clone https://github.com/ferriprasetya/wa-baileys-dashboard.git
cd wa-baileys-dashboard- Environment Setup
Copy the example environment file:
cp .env.example .envEnsure DATABASE_URL and REDIS_HOST match your local setup.
Generate Session Key (Critical): Run this command to generate a valid 32-byte key file. Do not use shell redirection (>) as it may corrupt the key with newlines.
node -e "require('fs').writeFileSync('secret-key', require('crypto').randomBytes(32))"- Start Infrastructure (DB & Redis)
Run PostgreSQL and Redis via Docker:
docker compose up -d postgres redis- Install Dependencies
npm install- Database Migration & Seeding
Push the schema to the database and seed initial data (Admin user):
npm run db:generate
npm run db:migrate
npm run db:seed # (Optional: creates default admin)- Run Development Server
Start the app with hot reload:
npm run devAccess the dashboard at http://localhost:3000.
npm run dev- Start development server with tsx watchnpm run build- Compile TypeScript todist/npm start- Run production buildnpm run format- Format code using Prettier & ESLint
npm run db:generate- Generate SQL migrations from Drizzle schemanpm run db:migrate- Apply migrations to the databasenpm run db:seed- Seed initial data (admin user)
npm run docker:dev- Start development environment with Docker Composenpm run docker:dev:down- Stop development environmentnpm run docker:dev:logs- View logs from development containernpm run docker:prod- Start production environment with Docker Composenpm run docker:prod:down- Stop production environmentnpm run docker:prod:logs- View logs from production containernpm run docker:prod:seed- Run seeder in production container
Generate the key on your host machine (Production Server) once:
node -e "require('fs').writeFileSync('secret-key', require('crypto').randomBytes(32))"To run the full application (App + DB + Redis) in a production simulation:
docker compose up --buildAfter starting the production environment, run migrations and seed the database:
# Apply database migrations
npm run docker:prod:migrate
# Seed initial data (creates default admin user)
npm run docker:prod:seedOr manually inside the running container:
docker compose exec app npm run db:migrate
docker compose exec app npm run db:seedThis section explains how to integrate the WhatsApp Gateway with external services (e.g., Nuxt applications, microservices, etc.).
Send messages to WhatsApp via HTTP POST request.
Endpoint: POST /public/api/send
Getting Credentials:
- Log in to the dashboard at
http://localhost:3000 - Navigate to the Tenant List section
- Click the "View Credentials" button on the desired tenant
- Copy the
Tenant IDandAPI Keyfrom the modal dialog
Request Body:
{
"tenantId": "YOUR_TENANT_ID_HERE",
"apiKey": "YOUR_API_KEY_HERE",
"to": "628123456789",
"message": "Your message here with optional formatting"
}Request Body Parameters:
| Parameter | Type | Description |
|---|---|---|
tenantId |
string | Unique identifier for the WhatsApp tenant/session |
apiKey |
string | API key for authenticating the request |
to |
string | Recipient phone number (format: 628xxx...) |
message |
string | Message content (supports formatting) |
Request Headers:
Content-Type: application/json
Response (200 OK):
{
"status": "queued",
"jobId": "12345",
"queuePosition": 5
}Response (404 Not Found):
{
"error": "Tenant not found"
}Response (401 Unauthorized):
{
"error": "Invalid API Key"
}Message Formatting Support:
WhatsApp supports the following text formatting:
*text*→ bold_text_→ italic~text~→strikethrough`text`→monospace\n→ newline
Example Request with Formatting:
{
"tenantId": "YOUR_TENANT_ID_HERE",
"apiKey": "YOUR_API_KEY_HERE",
"to": "628123456789",
"message": "*Notifikasi Penting*\n\nAnda memiliki tugas baru:\n_Submission Deadline: 2024-01-31_\n\nSilakan login untuk informasi lebih lanjut."
}Connect to WhatsApp sessions in real-time using WebSocket to receive QR codes and connection status updates.
Endpoint: WS /public/tenants/:tenantId/ws?apiKey=YOUR_API_KEY
Connection Example (JavaScript):
const tenantId = '01baba0d-a4cf-4cff-a5b4-5f5450699579'
const apiKey = '7e60eeffcdc7d96ed9c1fc3a57e80753942696139dc45b91af1251d07b47c367'
const ws = new WebSocket(`ws://localhost:3000/public/tenants/${tenantId}/ws?apiKey=${apiKey}`)
ws.onopen = () => {
console.log('Connected to WhatsApp session')
}
ws.onmessage = (event) => {
const message = JSON.parse(event.data)
switch (message.type) {
case 'qr':
// Display QR code for scanning
console.log('QR Code:', message.data)
displayQRCode(message.data)
break
case 'ready':
// Session is connected
console.log('WhatsApp connected as:', message.jid)
updateStatus('connected', message.jid)
break
case 'close':
// Session disconnected
console.log('WhatsApp session closed')
updateStatus('disconnected')
break
}
}
ws.onerror = (error) => {
console.error('WebSocket error:', error)
}
ws.onclose = () => {
console.log('Connection closed')
}Connection Example (TypeScript):
class WhatsAppSessionClient {
private socket: WebSocket | null = null
private tenantId: string
private apiKey: string
private reconnectAttempts = 0
private maxReconnectAttempts = 5
private reconnectDelay = 3000
constructor(tenantId: string, apiKey: string) {
this.tenantId = tenantId
this.apiKey = apiKey
}
connect(): void {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
console.warn('Already connected')
return
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const wsUrl = `${protocol}//${window.location.host}/public/tenants/${this.tenantId}/ws?apiKey=${this.apiKey}`
this.socket = new WebSocket(wsUrl)
this.socket.onopen = () => {
console.log('Connected to WhatsApp session')
this.reconnectAttempts = 0 // Reset on successful connection
this.onConnect?.()
}
this.socket.onmessage = (event) => {
const message = JSON.parse(event.data)
switch (message.type) {
case 'qr':
console.log('QR Code received')
this.onQRCode?.(message.data)
break
case 'ready':
console.log('WhatsApp session connected:', message.jid)
this.onReady?.(message.jid)
break
case 'close':
console.log('WhatsApp session closed')
this.onClose?.()
break
}
}
this.socket.onerror = (error) => {
console.error('WebSocket error:', error)
this.onError?.(error)
}
this.socket.onclose = () => {
console.log('WebSocket closed')
this.attemptReconnect()
}
}
private attemptReconnect(): void {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++
console.log(
`Attempting to reconnect... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
)
setTimeout(() => this.connect(), this.reconnectDelay)
} else {
console.error('Max reconnection attempts reached')
this.onMaxReconnectReached?.()
}
}
disconnect(): void {
if (this.socket) {
this.socket.close()
this.socket = null
}
}
// Event handlers (override these in your code)
onConnect?: () => void
onQRCode?: (qrImage: string) => void
onReady?: (jid: string) => void
onClose?: () => void
onError?: (error: Event) => void
onMaxReconnectReached?: () => void
}
// Usage example
const client = new WhatsAppSessionClient(
'01baba0d-a4cf-4cff-a5b4-5f5450699579',
'7e60eeffcdc7d96ed9c1fc3a57e80753942696139dc45b91af1251d07b47c367',
)
client.onConnect = () => {
console.log('Ready to receive events')
}
client.onQRCode = (qrImage: string) => {
// Display QR code to user
const img = document.getElementById('qr-code') as HTMLImageElement
if (img) {
img.src = qrImage
img.style.display = 'block'
}
}
client.onReady = (jid: string) => {
console.log('Session connected as:', jid)
// Hide QR code and show success
const img = document.getElementById('qr-code') as HTMLImageElement
if (img) img.style.display = 'none'
}
client.onClose = () => {
console.log('Session disconnected')
}
client.onError = (error: Event) => {
console.error('Connection error:', error)
}
// Connect to session
client.connect()
// Disconnect when needed
// client.disconnect()WebSocket Message Types:
| Type | Description | Payload |
|---|---|---|
qr |
QR code generated | { type: 'qr', data: '...' } (base64 image) |
ready |
Session connected | { type: 'ready', jid: '...' } (WhatsApp JID) |
close |
Session disconnected | { type: 'close' } |
Connection States:
- Awaiting QR Scan: Client receives
qrmessage with QR code image. User scans with WhatsApp to authenticate. - Connected: After successful scan, client receives
readymessage with WhatsApp JID (phone number). - Auto-reconnect on Disconnect: If connection is interrupted, client can manually reconnect by re-establishing WebSocket.
This project is licensed under the MIT License.