Building a Self Hosted WhatsApp Bot

Building a Self Hosted WhatsApp Bot

What You’ll Need

Table of Contents

Understanding the Architecture

Commercial WhatsApp Business APIs often require expensive monthly subscriptions, verification processes, and per-message charges. When building internal notifications, custom CRM helpers, or interactive personal bots, setting up a self-hosted client backend using WebSockets gives you complete control over your message infrastructure without recurring vendor usage fees.

Our setup relies on a persistent headless Node.js service using Baileys, a lightweight library that communicates directly with WhatsApp Web protocols over WebSockets. We pair this with an Express HTTP engine, enabling external applications to push messages or consume incoming triggers securely.

To maintain continuous connectivity without manual QR code scans every time a container restarts, we store persistent authentication tokens on disk using local volume mounts. When hosting your daemon on a high-uptime server like a Hetzner VPS or Contabo VPS, your bot runs continuously while consuming minimal CPU and RAM resources.

+-------------------------------------------------------------+
|                      Cloud Provider                         |
|  +-------------------+              +--------------------+  |
|  |   Express REST    | <==========> | Baileys WASocket   |  |
|  |   API (Port 3000) |              | Engine             |  |
|  +---------+---------+              +---------+----------+  |
|            ^                                  ^             |
+------------|----------------------------------|-------------+
             v                                  v
+----------------------+              +--------------------+
| External Automation  |              | WhatsApp Web       |
| Server / Webhooks    |              | Network Protocol   |
+----------------------+              +--------------------+

Setting Up Node.js and WhatsApp Session Management

To begin, we construct a unified Node.js backend. This service initializes the Baileys client socket, manages reconnect signals, prints authentication QR codes to stdout on first boot, and exposes HTTP REST endpoints to send messages programmatically.

Create a new workspace directory on your machine or target server, then install the necessary dependencies:

mkdir whatsapp-bot
cd whatsapp-bot
npm init -y
npm install @whiskeysockets/baileys qrcode-terminal express http

Create a file named bot.js. Paste the following complete, fully executable implementation directly into your workspace.

const { default: makeWASocket, useMultiFileAuthState, DisconnectReason } = require('@whiskeysockets/baileys');
const qrcode = require('qrcode-terminal');
const express = require('express');
const http = require('http');

const app = express();
app.use(express.json());

let sock;

async function connectToWhatsApp() {
    const { state, saveCreds } = await useMultiFileAuthState('auth_info_baileys');
    
    sock = makeWASocket({
        printQRInTerminal: true,
        auth: state
    });

    sock.ev.on('creds.update', saveCreds);

    sock.ev.on('connection.update', (update) => {
        const { connection, lastDisconnect, qr } = update;
        if (qr) {
            qrcode.generate(qr, { small: true });
        }
        if (connection === 'close') {
            const statusCode = lastDisconnect?.error?.output?.statusCode;
            const shouldReconnect = (statusCode !== DisconnectReason.loggedOut);
            console.log('Connection closed. Reconnecting:', shouldReconnect);
            if (shouldReconnect) {
                connectToWhatsApp();
            }
        } else if (connection === 'open') {
            console.log('WhatsApp connection established successfully.');
        }
    });

    sock.ev.on('messages.upsert', async (m) => {
        const msg = m.messages[0];
        if (!msg.key.fromMe && m.type === 'notify') {
            const sender = msg.key.remoteJid;
            const text = msg.message?.conversation || msg.message?.extendedTextMessage?.text || '';
            console.log(`Received message from ${sender}: ${text}`);

            if (text.toLowerCase() === '!ping') {
                await sock.sendMessage(sender, { text: 'pong' });
            } else if (text.toLowerCase() === '!status') {
                await sock.sendMessage(sender, { text: 'Server is online and healthy.' });
            }
        }
    });
}

app.post('/send-message', async (req, res) => {
    const { number, message } = req.body;
    if (!number || !message) {
        return res.status(400).json({ error: 'Missing required parameters: number, message' });
    }
    try {
        const formattedJid = number.includes('@s.whatsapp.net') ? number : `${number}@s.whatsapp.net`;
        await sock.sendMessage(formattedJid, { text: message });
        return res.status(200).json({ status: 'success', sentTo: formattedJid });
    } catch (error) {
        return res.status(500).json({ error: error.message });
    }
});

const PORT = process.env.PORT || 3000;
http.createServer(app).listen(PORT, () => {
    console.log(`Express REST listener running on port ${PORT}`);
    connectToWhatsApp();
});

When started, the script creates a folder named auth_info_baileys. This directory retains session keys, encryption tokens, and state parameters. Upon your initial run, scan the displayed QR terminal prompt using your physical device inside WhatsApp (Settings > Linked Devices). Subsequent server boots will automatically read state data from this local directory and skip the pairing flow entirely.

๐Ÿ’ก Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.

Building the Webhook Forwarder and Command Router

Receiving messages directly on your daemon is great for simple ping commands, but modern architectures require pushing incoming payload events to background microservices. To keep your API high-performing, message ingestion must be asynchronous and decoupled from business logic processing.

If your bot receives thousands of messages during spike events, forwarding them synchronously can lock your Node event loop. Read our detailed guide on Designing Resilient Webhook Endpoints with Redis Queues to implement queue patterns that prevent dropouts under high concurrency.

Additionally, because we expose an Express REST endpoint (/send-message) to allow external applications to issue WhatsApp messages, securing this route from unauthorized exploitation is paramount. You can protect your service endpoints using standard authentication strategies as explained in our article on Securing Microservice Endpoints With OAuth2 Bearer Tokens.

When configuring public-facing endpoints across multiple message channels, note that each provider handles payload verification differently. If you are comparing WhatsApp to other platforms, read How To Secure Telegram Bot Webhook Endpoints to understand HMAC verification patterns.

Below is an isolation relay script named relay.js. This module sits between your main WhatsApp client and your internal backend infrastructure, validating tokens and forwarding parsed payloads safely.

const express = require('express');
const axios = require('axios');

const app = express();
app.use(express.json());

const TARGET_WORKFLOW_URL = process.env.DISPATCH_WEBHOOK_URL || 'http://localhost:5000/webhook';
const BEARER_TOKEN = process.env.API_SECRET_KEY || 'supersecretkey123';

app.post('/incoming-whatsapp', async (req, res) => {
    const authHeader = req.headers['authorization'];
    if (!authHeader || authHeader !== `Bearer ${BEARER_TOKEN}`) {
        return res.status(401).json({ error: 'Unauthorized request token' });
    }

    const { sender, message, timestamp } = req.body;

    try {
        const payload = {
            event: 'whatsapp_message_received',
            from: sender,
            content: message,
            receivedAt: timestamp || new Date().toISOString()
        };

        const response = await axios.post(TARGET_WORKFLOW_URL, payload, {
            headers: {
                'Content-Type': 'application/json',
                'X-Bot-Signature': 'verified-internal-origin'
            },
            timeout: 5000
        });

        return res.status(200).json({ status: 'dispatched', upstreamStatus: response.status });
    } catch (error) {
        return res.status(500).json({ error: 'Webhook delivery failed', details: error.message });
    }
});

app.listen(4000, () => {
    console.log('Webhook relay proxy executing on port 4000');
});

To run this pipeline efficiently, your core WhatsApp engine formats raw WebSocket events and posts them directly to your backend relay using standard JSON payloads.

Containerizing and Deploying on a VPS

Deploying your self-hosted solution to production requires isolated process controls, log recycling, and persistent volume management. Docker guarantees that session tokens remain untouched during application upgrades.

We start by building a minimal Node environment via Dockerfile:

FROM node:18-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install --production

COPY . .

EXPOSE 3000

CMD ["node", "bot.js"]

Next, configure multi-container execution and persistent disk storage using docker-compose.yml. Mount the local session folder into the container workspace so auth keys persist across container restarts.

version: '3.8'

services:
  whatsapp-bot:
    build: .
    container_name: whatsapp_bot_service
    restart: always
    ports:
      - "3000:3000"
    volumes:
      - ./auth_info_baileys:/usr/src/app/auth_info_baileys
    environment:
      - PORT=3000
      - NODE_ENV=production

When running on cloud infrastructure hosted on DigitalOcean or Hetzner VPS, launch the service in detached mode:

docker-compose up -d --build

To scan the initial QR code while running inside Docker detached mode, stream the container logs directly to your terminal screen:

docker-compose logs -f whatsapp-bot

After scanning the QR code, press Ctrl+C to return to your normal prompt. The authorization payload is saved safely within ./auth_info_baileys on your server host disk.

+------------------------------------------------------------------+
|                  HOST SERVER DIRECTORY STRUCTURE                 |
|                                                                  |
|  /opt/whatsapp-bot/                                              |
|  โ”œโ”€โ”€ Dockerfile                                                  |
|  โ”œโ”€โ”€ docker-compose.yml                                          |
|  โ”œโ”€โ”€ package.json                                                |
|  โ”œโ”€โ”€ bot.js                                                      |
|  โ””โ”€โ”€ auth_info_baileys/  <=== Persistent Docker Volume Mount   |
|      โ”œโ”€โ”€ creds.json                                              |
|      โ”œโ”€โ”€ app-state-sync-key-xxx.json                             |
|      โ””โ”€โ”€ session-xxx.json                                        |
+------------------------------------------------------------------+

Getting Started

Building your own self-hosted WhatsApp integration provides speed, absolute privacy control, and eliminates high per-message usage costs. By standardizing message distribution using REST hooks, you can link this daemon directly into n8n Cloud or self-hosted automation instances to handle customer service tickets, system monitoring notifications, or database alerts.

If you need a server environment to spin up your bot daemon right away, configure a high-performance instance on Hetzner VPS, Contabo VPS, or DigitalOcean. Register a distinct domain via Namecheap if you plan on putting your API endpoints behind an SSL proxy like Nginx or Caddy.

Outsource Your Automation

Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr, mention SYS3-HUGO for priority. Or DM at chasebot.online.

Want to automate this yourself?

Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.

Want this engine running on your own VPS?

This blog publishes itself โ€” daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.

Get System 3
system online