Skip to content

Architecture

PyWacli bridges WhatsApp → Node.js (Baileys) → Python, persisting everything to a SQL database and syncing media to object storage, with a live terminal UI on top.

flowchart TD
  WA[WhatsApp mobile] -- Baileys library --> NODE

  subgraph Node.js
    NODE[baileys_service.js]
    WS[ws_server.js / command server]
    SM[session_manager.js\nsocket singleton]
    NODE --- SM
    NODE --- WS
  end

  NODE -- normalized JSON events --> PY[Python event processor]

  subgraph Python
    PY --> DB[(SQL database\nSQLite / Postgres / MySQL)]
    PY --> STORE[Media storage\nS3 / R2 / B2 / local]
    DASH[Rich dashboard]
    DB --> DASH
  end

  SEND[Send / Automate] -- local command server --> WS

Components

Component Language Role
baileys_service.js Node.js Connects to WhatsApp via Baileys, downloads media, normalizes events
session_manager.js Node.js Holds the single Baileys socket as a singleton
ws_server.js Node.js WebSocket / local command server for outgoing messages
event_processor.py Python Consumes normalized events and routes them to the DB and storage
websocket_services.py Python WebSocket consumer for real-time events
message_sender.py Python Sends outgoing messages through the live session
db/ Python SQLAlchemy engine, schema, and read/write queries
ai_engine/ Python Provider factory, prompt templates, and skills for AI Automate
cli/ui/dashboard.py Python The live Rich dashboard

Data flow

  1. Capture — Baileys receives WhatsApp events; the Node bridge normalizes them into clean JSON and writes them to stdout (and downloads media files).
  2. Process — the Python event processor reads those events and persists them to the database; media is uploaded to each configured storage target.
  3. Display — the dashboard reads from the database and re-renders on an interval.
  4. Send — Send/Automate push outgoing messages to the local command server, which forwards them through the one live socket.

The single-socket model

WhatsApp allows one linked-device socket per session. PyWacli embraces this: connect owns that socket and exposes a small local command server (send_host/send_port, default 127.0.0.1:8765). Every other action coordinates through it — so you never risk two competing connections.