Most WhatsApp automation guides end with "sign up for this SaaS." This one doesn't. I'll walk you through setting up a fully self-hosted WhatsApp customer support system using three open-source tools — no per-message fees, no per-seat pricing, no vendor lock-in.
The Stack
| Tool | Role | Replaces |
|---|---|---|
| n8n | Workflow automation | Zapier, Make.com |
| Chatwoot | Customer support platform | Intercom, Zendesk |
| WAHA | WhatsApp API (self-hosted) | Twilio, 360dialog |
Total hosting cost: ~$50/month on a basic VPS (2GB RAM, 1 vCPU). Compare that to $200+/month for equivalent SaaS tools.
Why This Combination Works
Each tool does one thing well:
- WAHA connects to WhatsApp and exposes a REST API for sending/receiving messages. No Meta Business verification required, no per-message charges.
- Chatwoot gives your support team a proper inbox — conversation routing, canned responses, agent assignments, performance dashboards. Your team works in Chatwoot, not in WhatsApp directly.
- n8n ties everything together — routing logic, AI responses, CRM updates, notifications. It's the "brain" that decides what happens when a message arrives.
The key insight: WAHA handles the transport, Chatwoot handles the human interface, and n8n handles the logic. Clean separation of concerns.
Architecture Overview
Customer (WhatsApp)
↓
WAHA (receives message via webhook)
↓
n8n (processes: route, enrich, auto-reply, or escalate)
↓
Chatwoot (agent sees conversation, responds)
↓
n8n (catches outgoing message)
↓
WAHA (sends reply back to WhatsApp)
Messages flow through n8n in both directions. This gives you full control over what happens at each step — you can add AI classification, auto-responses for FAQs, business hours logic, CRM lookups, or anything else.
Prerequisites
- A VPS with Docker and Docker Compose (Ubuntu 22.04+ recommended)
- A spare phone number for WhatsApp (or use your existing one)
- Basic familiarity with Docker
Step 1: Set Up WAHA
WAHA runs as a Docker container and connects to WhatsApp Web:
# docker-compose.waha.yml
services:
waha:
image: devlikeapro/waha-plus:latest
container_name: waha
restart: unless-stopped
ports:
- "3000:3000"
environment:
- WHATSAPP_DEFAULT_ENGINE=WEBJS
- WAHA_DASHBOARD_ENABLED=true
- WAHA_DASHBOARD_USERNAME=admin
- WAHA_DASHBOARD_PASSWORD=${WAHA_PASSWORD}
volumes:
- waha_data:/app/.sessions
volumes:
waha_data:
Start it:
docker compose -f docker-compose.waha.yml up -d
Open http://your-server:3000/dashboard, scan the QR code with your phone, and WAHA is connected.
Configure the webhook
In the WAHA dashboard (or via API), set the webhook URL to point to your n8n instance:
curl -X POST http://localhost:3000/api/sessions/default/webhooks \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-n8n-domain.com/webhook/whatsapp-incoming",
"events": ["message"]
}'
Step 2: Set Up Chatwoot
Chatwoot needs PostgreSQL and Redis:
# docker-compose.chatwoot.yml
services:
chatwoot-web:
image: chatwoot/chatwoot:latest
container_name: chatwoot-web
restart: unless-stopped
depends_on:
- chatwoot-postgres
- chatwoot-redis
ports:
- "3001:3000"
environment:
- RAILS_ENV=production
- SECRET_KEY_BASE=${CHATWOOT_SECRET}
- FRONTEND_URL=https://your-chatwoot-domain.com
- DATABASE_URL=postgresql://chatwoot:${DB_PASSWORD}@chatwoot-postgres:5432/chatwoot
- REDIS_URL=redis://chatwoot-redis:6379
command: bundle exec rails s -b 0.0.0.0 -p 3000
chatwoot-worker:
image: chatwoot/chatwoot:latest
container_name: chatwoot-worker
restart: unless-stopped
depends_on:
- chatwoot-postgres
- chatwoot-redis
environment:
- RAILS_ENV=production
- SECRET_KEY_BASE=${CHATWOOT_SECRET}
- DATABASE_URL=postgresql://chatwoot:${DB_PASSWORD}@chatwoot-postgres:5432/chatwoot
- REDIS_URL=redis://chatwoot-redis:6379
command: bundle exec sidekiq -C config/sidekiq.yml
chatwoot-postgres:
image: postgres:15
container_name: chatwoot-postgres
restart: unless-stopped
environment:
- POSTGRES_USER=chatwoot
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=chatwoot
volumes:
- chatwoot_pg:/var/lib/postgresql/data
chatwoot-redis:
image: redis:7-alpine
container_name: chatwoot-redis
restart: unless-stopped
volumes:
- chatwoot_redis:/data
volumes:
chatwoot_pg:
chatwoot_redis:
After starting, run the database setup:
docker exec chatwoot-web bundle exec rails db:chatwoot_prepare
Create an API channel in Chatwoot
- Log in to Chatwoot → Settings → Inboxes → Add Inbox
- Choose API as the channel type
- Name it "WhatsApp"
- Save the Inbox ID and generate an API access token — you'll need both for n8n
Step 3: Set Up n8n
n8n connects everything:
# docker-compose.n8n.yml
services:
n8n:
image: n8nio/n8n:latest
container_name: n8n
restart: unless-stopped
ports:
- "5678:5678"
environment:
- N8N_HOST=your-n8n-domain.com
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://your-n8n-domain.com/
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=n8n-postgres
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=${N8N_DB_PASSWORD}
volumes:
- n8n_data:/home/node/.n8n
depends_on:
- n8n-postgres
n8n-postgres:
image: postgres:15
container_name: n8n-postgres
restart: unless-stopped
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=${N8N_DB_PASSWORD}
- POSTGRES_DB=n8n
volumes:
- n8n_pg:/var/lib/postgresql/data
volumes:
n8n_data:
n8n_pg:
Step 4: Build the Workflow in n8n
Here's where it all comes together. You need two workflows:
Workflow 1: Incoming Messages (WhatsApp → Chatwoot)
-
Webhook node — listens at
/webhook/whatsapp-incoming - Function node — extracts sender phone, message text, media URLs
- HTTP Request node — creates/finds contact in Chatwoot via API
- HTTP Request node — creates conversation (or finds existing one)
- HTTP Request node — posts the message to Chatwoot
The core logic in the Function node:
const body = $input.first().json.body;
const payload = body.payload;
return {
phone: payload.from.replace('@c.us', ''),
message: payload.body || '',
hasMedia: payload.hasMedia || false,
mediaUrl: payload.media?.url || null,
timestamp: payload.timestamp
};
Workflow 2: Outgoing Messages (Chatwoot → WhatsApp)
- Webhook node — Chatwoot sends a webhook when an agent replies
-
IF node — filter for
message_createdevents wheremessage_type === 'outgoing' - Function node — extract the reply text and recipient phone
- HTTP Request node — send via WAHA API:
// WAHA send message endpoint
const response = await $http.post('http://waha:3000/api/sendText', {
chatId: `${phone}@c.us`,
text: message,
session: 'default'
});
Bonus: Add AI Auto-Responses
Insert an AI Agent node between steps 2 and 3 in Workflow 1:
- Classify the message (FAQ, complaint, sales inquiry, urgent)
- For FAQ → auto-respond with a canned answer
- For everything else → route to Chatwoot for human handling
This handles the easy 60-70% of messages automatically while your team focuses on conversations that actually need a human.
What You Get
After setup, your support team sees this workflow:
- Customer sends a WhatsApp message
- n8n receives it, optionally auto-responds to FAQs
- If it needs a human — appears in Chatwoot inbox
- Agent responds in Chatwoot
- Reply goes back through n8n → WAHA → WhatsApp
Your team never touches WhatsApp directly. They work in a proper support tool with:
- Conversation assignment and routing
- Canned responses and macros
- Customer conversation history
- Performance metrics and SLA tracking
- Multi-agent support (no more "who's handling this?")
Production Considerations
Reverse proxy: Put all three services behind Caddy or Nginx with SSL. Each gets its own subdomain.
Backups: PostgreSQL databases need daily backups. A simple cron job with pg_dump works fine.
Monitoring: n8n has built-in execution logging. Set up a workflow that alerts you (via email or Telegram) if any workflow fails.
Scaling: This stack handles hundreds of conversations per day on a single $50/month VPS. If you need more, n8n supports Queue Mode with separate worker nodes.
Rate limits: WhatsApp has rate limits on messages. WAHA respects these automatically, but if you're sending bulk messages (marketing campaigns), add delays in your n8n workflows.
Cost Comparison
| Setup | Monthly Cost | Per-message fees | Per-seat fees |
|---|---|---|---|
| This stack (self-hosted) | ~$50 | None | None |
| Twilio + Intercom + Zapier | $300+ | Yes ($0.005+/msg) | Yes ($39+/seat) |
| MessageBird + Zendesk + Make | $250+ | Yes | Yes ($55+/seat) |
The self-hosted stack pays for itself after the first month if you have more than one support agent.
Wrapping Up
The n8n + Chatwoot + WAHA stack isn't the simplest setup — there's real DevOps involved. But once it's running, you get enterprise-grade WhatsApp support infrastructure at a fraction of the cost, with full control over your data.
I've been running this stack in production for multiple clients (clinics, e-commerce, service businesses) and it handles everything from appointment booking to order tracking to AI-powered FAQ responses.
If you want to see a live example of what this looks like in practice, I published a workflow template on n8n's community library that shows the pattern of connecting messaging APIs with AI processing.
Have questions about the setup? Drop a comment — happy to help with specific configuration issues.
Top comments (0)