DocHub
Complete system architecture — headless Chrome, Express backend, React frontend, PostgreSQL, Docker slices

WhatsApp Web Client — Complete Project Breakdown

This document describes the entire WhatsApp Web replacement application in sufficient detail to recreate it from scratch. Every file, every configuration choice, every architectural decision is documented here.


Table of Contents

  1. Architecture Overview
  2. Technology Stack
  3. Project Structure
  4. Infrastructure & Docker
  5. Database Schema
  6. Backend — Express + whatsapp-web.js
  7. Frontend — React + Vite + Tailwind
  8. Real-Time Communication
  9. Media Archival System
  10. Feature List & Implementation Details
  11. Complete File Reference

Architecture Overview

Phone (linked device)
    ↕
WhatsApp Servers
    ↕
Headless Chromium (inside Docker container)
    ↕ whatsapp-web.js
Express Backend (port 3101)
    ↕ REST API + Socket.io
React Frontend (port 3100)
    ↕ Vite dev proxy → backend
Browser

The system replaces the official WhatsApp Web interface. A headless Chromium browser runs inside a Docker container, maintaining a persistent WhatsApp Web session via whatsapp-web.js. The Express backend wraps this in a REST API and pushes real-time events via Socket.io. The React frontend provides the UI.

Key design decisions:

  • Session persists across container restarts via a Docker volume (wa_session_data)
  • Messages are archived to PostgreSQL as they flow through (offline access, search)
  • Media files are saved to disk organized by contact in categorized subfolders
  • All communication between frontend and backend uses relative /api URLs (no hardcoded hosts)
  • Frontend proxies API requests through Vite dev server to the backend container

Technology Stack

Backend

Technology Version Purpose
Node.js 20 (Alpine) Runtime
TypeScript ^5.3.3 Language
Express ^4.18.2 HTTP framework
whatsapp-web.js ^1.26.1-alpha.3 WhatsApp Web automation
Socket.io ^4.8.1 Real-time events
PostgreSQL 15 (Alpine) Message/chat archive
pg ^8.11.3 PostgreSQL client
Sharp ^0.33.5 Image thumbnail generation
qrcode ^1.5.4 QR code generation (data URI)
multer ^1.4.5-lts.1 File upload handling
helmet ^7.1.0 Security headers
morgan ^1.10.0 HTTP request logging
cors ^2.8.5 CORS configuration
nodemon ^3.0.2 Dev auto-restart

Frontend

Technology Version Purpose
React ^18.2.0 UI library
TypeScript ^5.2.2 Language
Vite ^5.0.0 Build tool + dev server
Tailwind CSS ^3.3.6 Utility-first CSS
React Router ^6.20.1 Client-side routing
Axios ^1.6.2 HTTP client
socket.io-client ^4.8.1 WebSocket client
lucide-react ^0.294.0 Icon library
PostCSS ^8.4.32 CSS processing
autoprefixer ^10.4.16 CSS vendor prefixes

Infrastructure

Technology Version Purpose
Docker Compose v2 Container orchestration
Chromium Alpine package Headless browser for Puppeteer

Project Structure

WhatsApp/
├── docker-compose.yml              # 3 services: db, backend, frontend
├── .env                            # Environment variables
├── start.sh                        # docker compose up -d
├── stop.sh                         # docker compose down
├── media/                          # Persistent media storage (volume-mounted)
│   └── +PHONE_ContactName/         # Per-contact folders
│       ├── photos/                 # .jpg, .png, .gif, .webp
│       ├── videos/                 # .mp4, .3gp
│       ├── docs/                   # .pdf, .docx, .xlsx, etc.
│       ├── audio/                  # .ogg, .mp3
│       ├── thumbs/                 # 200px JPEG thumbnails
│       ├── transcript.txt          # Chronological message log
│       └── links.json              # Extracted URLs
│
├── database/
│   └── init/
│       └── 01-schema.sql           # PostgreSQL schema (auto-runs on first start)
│
├── backend/
│   ├── Dockerfile                  # Multi-stage: dev (nodemon) + prod (compiled JS)
│   ├── package.json
│   ├── tsconfig.json
│   └── src/
│       ├── index.ts                # Express app + server setup
│       ├── config/
│       │   └── database.ts         # PostgreSQL connection pool
│       ├── services/
│       │   ├── WhatsAppService.ts  # Singleton — wraps whatsapp-web.js Client
│       │   ├── WebSocketService.ts # Singleton — Socket.io server
│       │   └── MediaService.ts     # Singleton — file organization + thumbnails
│       ├── controllers/
│       │   ├── statusController.ts # Connection state, initialize, logout
│       │   ├── chatController.ts   # Chat list, contact details
│       │   ├── messageController.ts# Message history, send text/media
│       │   └── mediaController.ts  # Media browser, sync, delete, thumbnails
│       └── routes/
│           ├── statusRoutes.ts
│           ├── chatRoutes.ts
│           ├── messageRoutes.ts
│           └── mediaRoutes.ts
│
└── frontend/
    ├── Dockerfile                  # Multi-stage: dev (Vite) + prod (nginx)
    ├── package.json
    ├── tsconfig.json
    ├── tsconfig.node.json
    ├── vite.config.ts              # Proxy /api → backend, port 3100
    ├── tailwind.config.js          # WhatsApp color palette
    ├── postcss.config.js
    ├── index.html
    └── src/
        ├── main.tsx                # React entry point
        ├── App.tsx                 # Router + layout (TopBar + BackfillBanner + Routes)
        ├── index.css               # Tailwind + IBM Plex font + scrollbar styles (14px base)
        ├── vite-env.d.ts
        ├── types/
        │   └── index.ts            # All TypeScript interfaces
        ├── services/
        │   ├── api.ts              # Axios API layer (status, chat, message, media, contact, personality)
        │   ├── actionsAPI.ts       # Actions/reminders API
        │   └── reminderAPI.ts      # Reminder CRUD API
        ├── contexts/
        │   └── WhatsAppContext.tsx  # Global state + socket event handling
        ├── hooks/
        │   ├── useSSE.ts            # SSE (Server-Sent Events) client hook
        │   └── useIdleBackfill.ts  # Idle-triggered backfill hook
        ├── pages/
        │   ├── LoginPage.tsx       # QR code display + connection UI
        │   ├── ChatPage.tsx        # 3-column grid: ChatList | Conversation | CrmPanel
        │   ├── SettingsPage.tsx     # Connection info + logout + storage
        │   ├── MediaBrowserPage.tsx # Full media browser with tabs/lightbox/delete
        │   ├── ActionListPage.tsx   # Global action/reminder list
        │   ├── PersonalitiesPage.tsx# AI personality management
        │   └── AISettingsPage.tsx   # AI provider configuration
        └── components/
            ├── TopBar.tsx          # Brand label + status + nav (redesigned)
            ├── ActionBar.tsx       # DEPRECATED — no longer imported
            ├── BackfillBanner.tsx  # Thin backfill progress bar
            ├── ChatList.tsx        # Filter chips + searchable contact list (redesigned)
            ├── ChatListItem.tsx    # Edge-to-edge avatar + tags + AI icon (redesigned)
            ├── ConversationView.tsx# Header + action banner + messages (redesigned)
            ├── CrmPanel.tsx        # NEW — Persistent CRM sidebar with sections
            ├── MessageBubble.tsx   # Bubbles with inline images + context menu
            ├── MessageComposer.tsx # Text + file input + reply preview + send
            ├── MessageContextMenu.tsx # Right-click/dropdown menu for messages
            ├── ContactProfile.tsx  # DEPRECATED — replaced by CrmPanel
            ├── ContactEditForm.tsx # Reusable contact edit form (used in CrmPanel)
            ├── PersonalityPicker.tsx # AI personality assignment (used in CrmPanel)
            ├── PersonalityBadge.tsx# AI personality badge display
            ├── AvatarPicker.tsx    # Avatar selection modal
            ├── ForwardModal.tsx    # Message forwarding modal
            ├── WhereWereWeModal.tsx# AI conversation summary
            ├── AnalysePanel.tsx    # Contact analysis
            ├── DuplicatesPanel.tsx # Duplicate contact detection
            ├── ContactImportExport.tsx # VCF import/export
            ├── MediaStorageSettings.tsx # Storage configuration
            ├── CollapsibleSection.tsx # Reusable collapsible UI
            ├── Tip.tsx             # Tooltip component
            └── QRCode.tsx          # QR code image display

Infrastructure & Docker

docker-compose.yml

Three services on a shared bridge network (wa-network):

1. wa-db (PostgreSQL 15)

  • Container name: whatsapp-db
  • Port: 5436:5432 (external:internal)
  • Volume: wa_db_data for persistent data
  • Init script: ./database/init/01-schema.sql runs on first creation
  • Credentials: wa_user / wa_password / database whatsapp

2. wa-backend (Node.js 20 + Chromium)

  • Container name: whatsapp-backend
  • Port: 3101:3101
  • shm_size: '1gb'critical for Chromium shared memory (prevents crashes)
  • Volumes:
    • ./backend:/app — live code editing with nodemon
    • /app/node_modules — anonymous volume (container’s own modules)
    • wa_session_data:/app/.wwebjs_authpersistent WhatsApp session (survives restarts, no re-scan)
    • ./media:/app/media — media file storage
  • Command: npm run dev (nodemon watches TypeScript files)
  • Environment: DATABASE_URL=postgresql://wa_user:wa_password@wa-db:5432/whatsapp
  • Depends on: wa-db

3. wa-frontend (Node.js 20 + Vite)

  • Container name: whatsapp-frontend
  • Port: 3100:3100
  • Volumes:
    • ./frontend:/app — live code editing with HMR
    • /app/node_modules — anonymous volume
  • Command: npm run dev
  • Depends on: wa-backend

Backend Dockerfile (Multi-stage)

Development stage:

FROM node:20-alpine AS development
RUN apk add --no-cache chromium nss freetype harfbuzz ca-certificates ttf-freefont dbus
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
  • Installs Chromium and font dependencies for Puppeteer
  • PUPPETEER_EXECUTABLE_PATH tells whatsapp-web.js to use the system Chromium
  • PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true prevents downloading a second Chromium

Production stage:

  • Compiles TypeScript to dist/
  • Runs as non-root user nodeuser
  • Production-only npm install

Frontend Dockerfile (Multi-stage)

Development stage: Plain Node.js with Vite dev server Production stage: nginx serving the built dist/ directory

.env

NODE_ENV=development
POSTGRES_DB=whatsapp
POSTGRES_USER=wa_user
POSTGRES_PASSWORD=wa_password
DATABASE_URL=postgresql://wa_user:wa_password@localhost:5436/whatsapp
BACKEND_PORT=3101
FRONTEND_PORT=3100

start.sh / stop.sh

Simple wrappers around docker compose up -d and docker compose down.


Database Schema

Four tables in PostgreSQL:

contacts

Column Type Notes
id SERIAL PK
wa_id VARCHAR(64) UNIQUE WhatsApp ID (e.g., 123456789@c.us)
phone_number VARCHAR(32)
push_name VARCHAR(255) Name set by user in WhatsApp
saved_name VARCHAR(255) Name from your phone contacts
profile_pic_url TEXT
is_group BOOLEAN
is_business BOOLEAN
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ

chats

Column Type Notes
id SERIAL PK
wa_id VARCHAR(64) UNIQUE
contact_id INTEGER FK References contacts(id)
name VARCHAR(255)
is_group BOOLEAN
unread_count INTEGER
last_message_text TEXT
last_message_at TIMESTAMPTZ
is_pinned BOOLEAN
is_archived BOOLEAN
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ

Index: idx_chats_last_message on last_message_at DESC

messages

Column Type Notes
id SERIAL PK
wa_id VARCHAR(128) UNIQUE Serialized message ID
chat_wa_id VARCHAR(64)
from_wa_id VARCHAR(64)
from_me BOOLEAN
body TEXT
message_type VARCHAR(32) Default ‘chat’
has_media BOOLEAN
media_mime_type VARCHAR(128)
media_url TEXT
media_filename VARCHAR(255)
ack INTEGER 0=pending, 1=sent, 2=delivered, 3=read
timestamp TIMESTAMPTZ
created_at TIMESTAMPTZ

Index: idx_messages_chat on (chat_wa_id, timestamp DESC)

connection_state (single-row)

Column Type Notes
id INTEGER PK Always 1 (CHECK constraint)
state VARCHAR(32) ‘disconnected’, ‘qr’, ‘connecting’, ‘ready’
phone_number VARCHAR(32)
last_connected_at TIMESTAMPTZ
updated_at TIMESTAMPTZ

Pre-seeded with one row: INSERT INTO connection_state (state) VALUES ('disconnected')


Backend

Entry Point (src/index.ts)

  1. Creates Express app with middleware: helmet (CSP disabled), cors (origin: localhost:3100), morgan (dev logging), express.json()
  2. Health check at GET /api/health
  3. Mounts route groups: /api/status, /api/chats, /api/messages, /api/media
  4. Initializes WebSocket service on the HTTP server
  5. Auto-initializes WhatsApp client 2 seconds after server start — if a saved session exists, it reconnects silently; if not, it waits for the user to click Connect

Database Config (src/config/database.ts)

PostgreSQL connection pool using pg:

  • Max 20 connections
  • 30s idle timeout, 2s connection timeout
  • Supports DATABASE_URL connection string or individual env vars
  • Exits process on unexpected pool errors

WhatsAppService (src/services/WhatsAppService.ts)

Singleton patternWhatsAppService.getInstance()

State machine: disconnectedconnectingqrready (or back to disconnected)

Key methods:

  • initialize() — Creates a new whatsapp-web.js Client with LocalAuth strategy and Puppeteer config. Cleans stale Chromium lock files first (from ungraceful shutdowns). Registers event handlers for: qr, ready, disconnected, authenticated, auth_failure, loading_screen, message, message_create, message_ack.
  • destroy() — Destroys the client instance
  • logout() — Logs out (clears session) then destroys
  • getClient() — Returns raw whatsapp-web.js Client for controllers
  • getState() / getPhoneNumber() — Current state accessors

Puppeteer configuration:

puppeteer: {
  headless: true,
  args: [
    '--no-sandbox',
    '--disable-setuid-sandbox',
    '--disable-dev-shm-usage',
    '--disable-accelerated-2d-canvas',
    '--no-first-run',
    '--disable-gpu',
  ],
  executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
}

Message handling (handleMessage):

  1. Skips status@broadcast messages
  2. Resolves contact info (phone + display name)
  3. Serializes message for DB and socket
  4. Archives to PostgreSQL (upsert by wa_id, updates ack on conflict)
  5. If media: downloads it, saves via MediaService, logs to transcript
  6. If text: appends to transcript, extracts links
  7. Emits socket event: wa:message (incoming) or wa:message_create (outgoing)

Contact resolution (resolveContact):

  • Gets pushname (WhatsApp display name), name (saved contact name), shortName
  • Falls back to phone number if no name available

Chromium lock cleanup (cleanChromiumLocks):

  • Walks .wwebjs_auth directory
  • Removes SingletonLock, SingletonCookie, SingletonSocket files
  • Prevents “already running” errors after ungraceful shutdown

WebSocketService (src/services/WebSocketService.ts)

Singleton pattern — wraps Socket.io server (used internally within each slice).

  • initialize(server) — Creates Socket.io server with CORS for localhost:3100
  • emit(event, data) — Broadcasts to all connected clients
  • Logs connect/disconnect events

Note: In the SaaS deployment, the browser does NOT connect to the slice’s Socket.io server directly. Instead, the gateway connects as a socket.io-client and re-emits events to the browser via SSE (Server-Sent Events). See Real-Time Communication.

MediaService (src/services/MediaService.ts)

Singleton pattern — handles all file organization and storage.

Media root: /app/media (mapped to ./media on host)

Contact directory naming: +PHONE_ContactName (e.g., +639625840831_Lanie)

  • Phone number with leading +
  • Name sanitized: only [a-zA-Z0-9 _-], spaces become underscores

File naming: +PHONE_ContactName_YYYYMMDD_HHMMSS.ext

MIME type mapping:

image/jpeg → .jpg     image/png → .png      image/gif → .gif
image/webp → .webp    video/mp4 → .mp4      video/3gpp → .3gp
audio/ogg → .ogg      audio/mpeg → .mp3     application/pdf → .pdf
text/plain → .txt     text/csv → .csv       (and more)

Category classification:

  • image/*photos/
  • video/*videos/
  • audio/*audio/
  • Everything else → docs/

Thumbnail generation:

  • Photos: Sharp resizes to 200x200px, cover fit, 60% JPEG quality
  • Videos: Uses WhatsApp’s embedded preview image (msg._data.mediaData?.preview?._b64), resizes to same spec
  • Stored in thumbs/ subfolder with .jpg extension
  • generateMissingThumbnails() backfills thumbnails for existing photos

Link extraction:

  • URL regex: https?:\/\/[^\s<>"{}|\\^[]]+`
  • Saved to links.json per contact with deduplication (url + timestamp)
  • Each entry: { url, timestamp, sender, context }

Transcript:

  • transcript.txt per contact
  • Format: [YYYY-MM-DD HH:MM:SS] SenderName: message text
  • Media entries: [time] Sender: [Media: filename.jpg] optional caption

Key methods:

  • saveMedia(contact, timestamp, mimetype, base64Data, previewB64?) → filename
  • saveLinks(contact, timestamp, sender, text) → count added
  • appendTranscript(contact, timestamp, senderName, text)
  • logMediaInTranscript(contact, timestamp, senderName, filename, caption?)
  • getCategorizedFiles(contactDir){ photos, videos, docs, audio } each with { name, size, modified }
  • getLinks(contactDir)LinkEntry[]
  • getAllFilenames(contactDir)Set<string> (for dedup during sync)
  • generateMissingThumbnails(contactDir) → number generated

Controllers

statusController (src/controllers/statusController.ts)

Endpoint Method Action
/api/status/state GET Returns { state, phoneNumber, lastConnectedAt } from service + DB
/api/status/initialize POST Starts WhatsApp client (non-blocking, returns immediately)
/api/status/logout POST Logs out and destroys client

chatController (src/controllers/chatController.ts)

Endpoint Method Action
/api/chats GET Lists all chats from WhatsApp client (falls back to DB if disconnected). Archives chats to DB in background. Returns { id, name, isGroup, unreadCount, lastMessage, timestamp, pinned, archived }[]
/api/chats/contact/:chatId GET Full contact details including profile pic URL, about text, business/enterprise flags, blocked status

Chat archiving: On each getChats call, upserts contacts and chats to PostgreSQL for offline access.

messageController (src/controllers/messageController.ts)

Endpoint Method Action
/api/messages/:chatId GET Fetch message history. ?limit=50. Timestamps converted from PostgreSQL TIMESTAMPTZ to Unix seconds. Media URLs enriched from media_files table or on-demand endpoint. Supplemented with DOM messages if chat is open.
/api/messages/:chatId/text POST Send text message. Body: { text }
/api/messages/:chatId/media POST Send media file. Multipart form: file + optional caption. Uses multer memory storage.
/api/messages/:chatId/react POST React to message. Body: { messageId, emoji }
/api/messages/:chatId/reply POST Reply to message. Body: { messageId, text }
/api/messages/:chatId/forward POST Forward message. Body: { messageId, targetChatId }
/api/messages/:chatId/star POST Star/unstar message. Body: { messageId }
/api/messages/:chatId/pin POST Pin message (7 days). Body: { messageId }
/api/messages/:chatId/delete POST Delete message from WhatsApp AND local DB. Body: { messageId, forEveryone }
/api/messages/:chatId/download-media POST Download media as JSON base64. Body: { messageId }
/api/messages/:chatId/media/:messageId GET Serve media inline as binary — fetches from WhatsApp on-demand, cached 24h

Media URL enrichment: When loading messages, the controller:

  1. Queries media_files table for synced thumbnails/originals
  2. Extracts folder + filename from absolute paths to build /api/media/thumb/ URLs
  3. For media messages without synced files, sets URL to the on-demand endpoint (/api/messages/:chatId/media/:messageId)

Delete cleanup: When a message is deleted, the controller also removes the row from the messages table and any associated media_files entries, preventing ghost messages from reappearing.

mediaController (src/controllers/mediaController.ts)

Endpoint Method Action
/api/media/contacts GET Lists all contact folders with categorized counts, total size, transcript line count
/api/media/contacts/:folder GET Full categorized file listing for one contact: photos, videos, docs, audio, links, transcript
/api/media/sync/:chatId POST Scans WhatsApp chat history, downloads missing media, extracts links. ?limit=100. Returns stats.
/api/media/file/:folder/:filename GET Serves a media file (checks subfolders then root)
/api/media/thumb/:folder/:filename GET Serves thumbnail (falls back to full file)
/api/media/file/:folder/:filename DELETE Deletes file + its thumbnail
/api/media/thumbs/:folder POST Generates missing thumbnails for a contact

Sync algorithm:

  1. Fetches N messages from WhatsApp chat
  2. For each message with media: checks if file already exists (by timestamp prefix)
  3. Downloads missing media via msg.downloadMedia()
  4. Saves to categorized subfolder, generates thumbnail
  5. Extracts links from all text messages
  6. Returns: { downloaded, skipped, failed, linksFound, byCategory, totalFilesInFolder }

Routes

Each route file creates an Express Router and maps endpoints to controller functions:

  • statusRoutes.ts: GET /state, POST /initialize, POST /logout
  • chatRoutes.ts: GET /, GET /contact/:chatId
  • messageRoutes.ts: GET /:chatId, POST /:chatId/text, POST /:chatId/media (with multer)
  • mediaRoutes.ts: GET /contacts, GET /contacts/:folder, POST /sync/:chatId, GET /file/:folder/:filename, GET /thumb/:folder/:filename, DELETE /file/:folder/:filename, POST /thumbs/:folder

Frontend

Build Configuration

vite.config.ts:

  • React plugin
  • Dev server on 0.0.0.0:3100 (accessible from any interface)
  • API proxy: /apihttp://wa-backend:3101 (Docker service name)
  • Path alias: @./src
  • Source maps enabled in build

tailwind.config.js — CRM Color Palette (redesigned 2026-02-19):

colors: {
  wa: {
    green: '#00c896',              // Primary accent (buttons, badges, active states)
    'green-dark': '#00a87e',       // Hover green
    'teal-green': '#005040',       // Deep green accents
    'light-green': '#dcf8c6',      // Light mode outgoing (unused in dark theme)
    bg: '#efeae2',                 // Light mode background (unused)
    'bg-deep': '#0e0f11',         // Page background
    panel: '#1c1f24',             // Raised panel/card
    'panel-header': '#15171a',    // Panel headers (same as sidebar)
    incoming: '#1c1f24',          // Incoming message bubble
    outgoing: '#003d2d',          // Outgoing message bubble
    'outgoing-border': '#005040', // Outgoing bubble border
    sidebar: '#15171a',           // Sidebar background
    hover: '#22262d',             // Hover state
    border: '#2a2f38',            // Border color
    'border-subtle': '#1e2228',   // Subtle borders
    text: '#e8ecf0',              // Primary text (bright white)
    'text-secondary': '#b0b8c4',  // Secondary text (readable grey)
    'text-muted': '#8891a0',      // Muted text (timestamps, labels)
    warn: '#f5a623',              // Warnings, action badges
    'warn-dim': 'rgba(245,166,35,0.12)', // Warning backgrounds
    danger: '#e05252',            // Errors, overdue, delete
    'accent-dim': 'rgba(0,200,150,0.12)', // Accent backgrounds
    'accent-border': 'rgba(0,200,150,0.3)', // Accent borders
  },
}
fontFamily: {
  sans: ['IBM Plex Sans', 'system-ui', '-apple-system', 'sans-serif'],
  mono: ['IBM Plex Mono', 'ui-monospace', 'monospace'],
}

postcss.config.js: Tailwind CSS + autoprefixer index.css: Tailwind directives + IBM Plex font import + scrollbar styling (8px general, 10px green-tinted for contact list via .contact-list-scroll class). Base font-size: 14px. index.html: Dark background, Google Fonts preconnect for IBM Plex Sans + Mono, title “OMELAS CRM”

TypeScript Types (src/types/index.ts)

Chat { id, name, isGroup, unreadCount, lastMessage: { body, timestamp, fromMe } | null, timestamp, pinned, archived }
Message { id, chatId, from, fromMe, body, type, hasMedia, timestamp, ack }
ConnectionState = 'disconnected' | 'qr' | 'connecting' | 'ready'
StatusInfo { state, phoneNumber, lastConnectedAt }
ContactDetail { id, number, name, shortName, pushname, isGroup, isBusiness, isEnterprise, isMyContact, isBlocked, profilePicUrl, about }
MediaFile { name, size, modified }
LinkEntry { url, timestamp, sender, context }
MediaContact { folder, counts: { photos, videos, docs, audio, links }, totalFiles, transcriptLines, totalSize }
MediaFolderDetail { folder, photos, videos, docs, audio, links, transcript }
SyncResult { chatId, contact, folder, totalMessages, mediaMessages, downloaded, skipped, failed, linksFound, newFiles, byCategory, totalFilesInFolder }

API Service Layer (src/services/api.ts)

Axios instance with base URL /api:

statusAPI:

  • getState() → GET /api/status/state
  • initialize() → POST /api/status/initialize
  • logout() → POST /api/status/logout

chatAPI:

  • getChats() → GET /api/chats
  • getContact(chatId) → GET /api/chats/contact/:chatId

messageAPI:

  • getMessages(chatId, limit=50) → GET /api/messages/:chatId
  • sendText(chatId, text) → POST /api/messages/:chatId/text
  • sendMedia(chatId, file, caption?) → POST /api/messages/:chatId/media (multipart)

mediaAPI:

  • listContacts() → GET /api/media/contacts
  • getContactMedia(folder) → GET /api/media/contacts/:folder
  • syncMedia(chatId, limit=100) → POST /api/media/sync/:chatId with {} body
  • getFileUrl(folder, filename) → URL string (no request)
  • getThumbUrl(folder, filename) → URL string (no request)
  • deleteFile(folder, filename) → DELETE /api/media/file/:folder/:filename
  • generateThumbs(folder) → POST /api/media/thumbs/:folder

Important: syncMedia sends {} as the body (not null). Express strict JSON mode rejects null.

SSE Hook (src/hooks/useSSE.ts)

  • Connects to GET /api/events via the native EventSource API
  • Plain HTTP (no WebSocket upgrade needed) — works through any reverse proxy
  • EventSource handles auto-reconnect natively with exponential backoff
  • Parses SSE data field as { event: string, payload: any }
  • Maintains listener map: Map<string, Set<Function>>
  • Returns { on, off, connected } — event emitter API used by WhatsAppContext

Replaced: The previous useWebSocket.ts hook connected directly to the slice’s Socket.io server. This was replaced with SSE because proxying socket.io through the gateway caused persistent 502 errors on WebSocket upgrades.

App Component (src/App.tsx)

Layout structure:

<BrowserRouter>
  <WhatsAppProvider>
    <div className="h-screen flex flex-col bg-wa-bg-deep">
      <TopBar />            {/* 44px — brand + status + nav */}
      <BackfillBanner />    {/* Thin progress bar during backfill */}
      <AppRoutes />         {/* flex-1 — page content */}
    </div>
  </WhatsAppProvider>
</BrowserRouter>

Note: ActionBar was removed in the 2026-02-19 redesign. Sync media moved to ConversationView header.

Routing:

  • When disconnected: only /settings and * → LoginPage
  • When connected: / → ChatPage, /media → MediaBrowserPage, /actions → ActionListPage, /personalities → PersonalitiesPage, /ai-settings → AISettingsPage, /settings → SettingsPage, * → redirect to /

WhatsAppContext (src/contexts/WhatsAppContext.tsx)

Global state provider — wraps entire app.

State:

  • connectionState — current WA connection status
  • phoneNumber — connected phone number
  • qrCode — base64 data URI for QR code
  • loadingProgress{ percent, message } during initialization
  • chats — full chat list
  • activeChat — currently selected chat
  • messages — messages for active chat

SSE event handlers (registered via on() from useSSE, bound once):

  • wa:catchup → hydrates full state on SSE connect (connectionState, qr, phoneNumber, loadingProgress, authenticated)
  • wa:qr → sets QR code, updates state to ‘qr’
  • wa:ready → clears QR, sets state to ‘ready’, fetches chats
  • wa:disconnected → clears everything
  • wa:loading → sets loading progress
  • wa:message → appends to messages (if active chat matches), bumps chat to top with unread count
  • wa:message_create → appends to messages (dedupes by ID)
  • wa:message_ack → updates ack value on matching message

Actions:

  • initialize() — sets state to ‘connecting’, calls API
  • logout() — calls API, clears all state
  • selectChat(chat) — fetches messages, clears unread
  • sendMessage(text) / sendMedia(file) — sends via API
  • checkState() — polls API for current state
  • resetUI() — cancels connecting state (UI reset only)
  • refreshChats() — re-fetches chat list

Pages

LoginPage

  • Shows when not connected
  • Three states: disconnected (green Connect button), connecting (spinner with progress bar), QR (QR code image)
  • Progress bar shows WhatsApp loading percentage from backend events

ChatPage (redesigned 2026-02-19)

Three-column CSS grid layout (300px | 1fr | 440px):

  • Left (300px): ChatList — contact list with filter chips, search, avatars
  • Center (1fr): ConversationView — messages + composer (or empty state)
  • Right (440px): CrmPanel — persistent CRM sidebar (or empty state)
  • AvatarPicker modal triggered by clicking avatars in contact list or CRM panel
  • Old ContactProfile overlay removed — replaced by persistent CrmPanel

SettingsPage

  • Connection status card
  • Phone number display
  • Logout button with confirmation

MediaBrowserPage (835 lines — largest component)

Left sidebar (288px):

  • Search bar filtering contacts by name or phone
  • Contact list showing: name, phone, categorized counts (photos/videos/docs/audio/links), total size
  • Selected state highlighted

Main content area:

  • Folder header with contact name and Sync Media button (spins while syncing)
  • Sync result banner showing: new downloads, skipped, failed, links found, breakdown by category
  • Tab bar: Photos | Videos | Docs | Audio | Links — each with count badge
  • Selection toolbar: Select/Cancel toggle, All/None, selected count + total size, red Delete N button

Tab content:

  • PhotoGrid: Responsive grid (3-8 columns by breakpoint), thumbnail images, hover overlay with file type/size/date, delete button on hover. Click opens lightbox. Select mode adds checkbox overlay with green ring.
  • VideoGrid: Same grid layout, uses video thumbnail (from WhatsApp preview), play button overlay, links to full video. Select mode same as photos.
  • FileList: List rows with extension badge, filename, type/size/date, download + delete on hover. Select mode adds checkbox column.
  • AudioList: Cards with <audio> player, file metadata, delete on hover. Select mode adds checkbox.
  • LinkList: Clickable URLs with sender, date, and surrounding context text.

Lightbox:

  • Full-screen black overlay
  • Top bar: filename, file type, size, date, download button, delete button, close button
  • Left/right navigation arrows
  • Counter: “N of M”

Multi-select:

  • Toggle with Select/Cancel button
  • Checkboxes appear on all items
  • Select All / Select None shortcuts
  • Bulk delete with confirmation dialog
  • Sequential API calls (one per file)
  • Selection clears on tab switch and folder switch

Helper functions:

  • parseFolder(folder) — extracts { name, phone } from +PHONE_Name format
  • displayFolder(folder) — returns "Name (+phone)" for headers
  • formatSize(bytes) — human-readable file sizes
  • formatDate(iso|timestamp) — locale-formatted date+time
  • getFileExt(name) — uppercase extension
  • getFileType(name) — human-readable type name (e.g., “JPEG Image”)

Components

TopBar (redesigned 2026-02-19)

44px fixed header with:

  • Left: “OMELAS CRM” brand label (mono, green), status pill (READY/CONNECTING/SCAN QR/OFFLINE with color coding), phone number, chat count, AWAY mode indicator
  • Right: Navigation buttons — Chats, Media, Actions (with count badge), Personalities, AI Settings, Away mode toggle, Settings, Logout (with confirmation modal)

All buttons use NavButton sub-component with: icon, label, accent/danger/active border styling. Labels hidden on small screens (hidden lg:inline).

Removed in redesign: ActionBar, Check State, Refresh Chats, Connect, Cancel buttons.

ChatList (redesigned 2026-02-19)

  • Header: “CONTACTS” label + total count
  • Search input
  • Filter chips: All, Warm, Action, New — filters by contact tags or pending reminders
  • Archived chats toggle (shows count, button to switch view)
  • Scrollable list with .contact-list-scroll class (10px green scrollbar)
  • List of ChatListItem components
  • Loads tags and reminders for all contacts on mount
  • Empty states for no chats / no matches / no filter results

ChatListItem (redesigned 2026-02-19)

  • Edge-to-edge square avatar (60px wide, no border/radius/padding, fills full row height). Click opens AvatarPicker.
  • Active indicator: 2px green bar between avatar and body
  • Body: Name (text-sm medium) + last message preview (text-xs) + tag chips (Warm/New/Cold/Action with color coding)
  • Right column: Timestamp (mono text-[11px]) + red robot icon (when AI active) + unread dot (green) + ACT badge (amber, when has pending actions)
  • Right-click context menu: pin/unpin
  • Props: chat, isActive, onClick, onTogglePin, onAvatarClick, avatarRetry, personalityEmoji, personalityName, personalityPaused, tags, hasActions

ConversationView (redesigned 2026-02-19)

  • Header: Name (text-sm semibold) + pushname/phone subline (mono text-[11px]) + personality badge + action count button + sync media button
  • Action banner: Amber bar below header when contact has pending reminders — shows first action text + “DONE” button
  • Scrollable message area with subtle dot pattern background
  • Auto-scrolls to bottom on new messages
  • MessageComposer at bottom
  • ForwardModal for message forwarding

CrmPanel (NEW — 2026-02-19)

Persistent right sidebar replacing old ContactProfile overlay. Sections:

  • Header: “CRM Profile” label
  • Hero: Large avatar (w-24 h-24, clickable for AvatarPicker) + name + pushname + phone + tag chips
  • Quick action buttons: Add Action, Note, Tag (pinned below hero)
  • Actions & Reminders (collapsible): List of pending reminders with complete/delete, inline add form
  • Photos (collapsible): 4-column thumbnail grid from synced media + “Browse All Media” button
  • Contact Details (collapsible): Wraps ContactEditForm component
  • AI Auto-Reply (collapsible): Wraps PersonalityPicker component
  • Activity (collapsible): “Where were we?” button + recent action timeline
  • Section headers: large emoji icon + uppercase mono title (text-sm font-semibold) + count badge + chevron toggle
  • Props: chatId, chatName, onAvatarPickerClick

MessageBubble

  • Left-aligned (incoming, bg-wa-incoming) or right-aligned (outgoing, bg-wa-outgoing)
  • Rounded corners with “tail” (tl-none for incoming, tr-none for outgoing)
  • Inline images: MediaPlaceholder shows images/videos inline when mediaUrl is available (from synced thumbnails or on-demand WhatsApp fetch). Falls back to icon + type label.
  • AI reply indicator: blue Bot icon + “AI” label
  • Hover dropdown arrow for context menu (reply, react, star, forward, copy, delete)
  • Timestamp in bottom-right (text-[11px])
  • Ack icons for outgoing: clock (0), single check (1), double check (2), blue double check (3+)
  • Star icon shown for starred messages

MessageComposer

  • Paperclip button → hidden file input
  • Textarea (auto-height up to 120px, Enter sends, Shift+Enter newline)
  • Reply preview bar (when replying to a message) with cancel button
  • Send button (disabled when empty)

ContactProfile (DEPRECATED)

File still exists but no longer used in the UI. Replaced by persistent CrmPanel in the right column.

QRCode

  • White background padding
  • 288px QR code image from data URI
  • Instruction text below

Real-Time Communication

Architecture (SSE + socket.io-client bridge)

Browser ──── GET /api/events (SSE) ────→ Gateway ──── socket.io-client ────→ Slice (localhost:500X)
         plain HTTP, text/event-stream      │         socket.io on localhost
         auto-reconnect built in            │         always works (no proxy)
                                            │
                                    SliceConnector
                                    (per-slice bridge)

The browser connects to the gateway via SSE (Server-Sent Events) — plain HTTP, no WebSocket upgrade needed. The gateway maintains a SliceConnector per active slice that connects to the slice’s Socket.io server as a client on localhost (always reliable). Events from the slice are re-emitted to all connected SSE browser streams.

Key components:

  • gateway/src/sse.ts — SliceConnector class + SSE endpoint handler
  • frontend/src/hooks/useSSE.ts — EventSource hook (replaces useWebSocket)
  • Slice backends keep their Socket.io servers unchanged

Events (Slice → Gateway → Browser)

Event Data Purpose
wa:catchup Full cached state Sent on SSE connect — immediate state hydration
wa:qr { qr: string } QR code as base64 data URI
wa:ready { phoneNumber: string } Connection established
wa:disconnected { reason: string } Connection lost
wa:loading { percent: number, message: string } WhatsApp loading progress
wa:auth_failure { message: string } Authentication failed
wa:phone_mismatch { previousPhone, newPhone } Different phone detected
wa:message Message object Incoming message from others
wa:message_create Message object Outgoing message confirmed
wa:message_ack { messageId: string, ack: number } Delivery status update
wa:message_reaction { messageId, chatId, reactions } Reaction added/removed

Connection Flow

  1. Browser opens GET /api/events → SSE stream established
  2. Gateway creates/reuses SliceConnector → connects to slice via socket.io-client
  3. SliceConnector fetches real state from slice HTTP API → sends wa:catchup to browser
  4. If saved session exists → authenticates silently → wa:ready emitted
  5. If no session → LoginPage auto-initializes → wa:qr emitted → user scans → wa:ready
  6. Frontend receives wa:ready → fetches chat list → shows ChatPage
  7. Real-time messages flow through SSE events while connected
  8. 15-second heartbeat comments (:keepalive) prevent proxy timeouts
  9. If SSE drops, EventSource auto-reconnects and gets fresh wa:catchup

Media Archival System

How Media Gets Saved

Real-time (via WhatsAppService message handler):

  1. Every incoming/outgoing message triggers handleMessage()
  2. If msg.hasMedia, downloads via msg.downloadMedia()
  3. Grabs WhatsApp’s video preview: msg._data.mediaData?.preview?._b64
  4. Saves to MEDIA_ROOT/+PHONE_Name/CATEGORY/filename.ext
  5. Generates thumbnail for photos (Sharp) or saves video preview
  6. Logs to transcript.txt
  7. Extracts links from text body

Manual sync (via mediaController.syncMedia):

  1. User clicks Sync Media → API fetches last N messages from WhatsApp
  2. Checks existing filenames to skip duplicates
  3. Downloads missing media
  4. Returns statistics

File Organization

/app/media/
  +639625840831_Lanie/
    photos/
      +639625840831_Lanie_20260215_143022.jpg
      +639625840831_Lanie_20260215_150101.png
    videos/
      +639625840831_Lanie_20260216_091533.mp4
    audio/
      +639625840831_Lanie_20260216_120045.ogg
    docs/
      +639625840831_Lanie_20260217_083012.pdf
    thumbs/
      +639625840831_Lanie_20260215_143022.jpg   ← 200px thumbnail
      +639625840831_Lanie_20260216_091533.jpg   ← video preview thumbnail
    transcript.txt
    links.json

Backwards Compatibility

The system supports both:

  • New layout: Files in categorized subfolders (photos/, videos/, etc.)
  • Old flat layout: Files directly in the contact root directory

All file lookup operations check subfolders first, then fall back to root.


Feature List & Implementation Details

1. WhatsApp Connection Management

  • QR code scanning via browser UI
  • Session persistence across container restarts (Docker volume)
  • Auto-reconnect on startup
  • Clean Chromium lock file cleanup
  • Connection state machine: disconnected → connecting → qr → ready
  • Loading progress display with percentage

2. Chat Interface

  • Full chat list with search filtering
  • Archive/unarchive toggle
  • Unread message badges
  • Last message preview with smart timestamps
  • Chat selection with message history loading
  • Real-time message delivery (no polling)

3. Messaging

  • Send and receive text messages
  • Send files via multipart upload
  • Message bubbles with left/right alignment
  • Delivery status ticks (clock → check → double check → blue double check)
  • Auto-scroll to newest message
  • Shift+Enter for newlines

4. Contact Profiles

  • Slide-over panel with full contact details
  • Profile picture display (with fallback icon)
  • About text
  • Business/Enterprise/Blocked flags
  • Accessible by clicking any avatar

5. Media Browser

  • Contact sidebar with search
  • Five tabs: Photos, Videos, Docs, Audio, Links
  • Photo grid with thumbnails (responsive columns)
  • Video grid with preview thumbnails (from WhatsApp)
  • Audio player with native <audio> controls
  • Document list with extension badges
  • Link list with context and sender info
  • File metadata on all items: type, size, date

6. Photo Lightbox

  • Full-screen image viewer
  • Left/right navigation
  • Image counter
  • Download button
  • Delete button
  • Keyboard-friendly (click overlay to close)

7. Media Sync

  • Sync from Chat view (ActionBar) or Media Browser
  • Downloads missing media from WhatsApp message history
  • Extracts URLs from text messages
  • Skips already-downloaded files
  • Shows detailed statistics: new, skipped, failed, by category

8. File Management

  • Single file delete with confirmation
  • Multi-select mode with checkboxes
  • Select All / Select None
  • Bulk delete with progress
  • Automatic thumbnail cleanup on delete
  • Sidebar counts refresh after changes
  • Automatic message logging to transcript.txt
  • URL extraction from all text messages
  • links.json with deduplication, sender info, context
  • Searchable link listing in Media Browser

10. Database Archiving

  • Messages archived in real-time as they flow through
  • Chat list cached on every fetch
  • Contacts upserted with latest names
  • Connection state persisted
  • Falls back to DB data when WhatsApp is disconnected

Complete File Reference

Infrastructure Files

File Lines Purpose
docker-compose.yml 69 Three services, two volumes, bridge network
.env 13 Environment variables
start.sh 11 Start all containers
stop.sh 5 Stop all containers
database/init/01-schema.sql 68 PostgreSQL schema with 4 tables + indexes

Backend Files

File Lines Purpose
backend/Dockerfile 75 Multi-stage: dev + build + prod with Chromium
backend/package.json 39 12 dependencies + 11 devDependencies
backend/tsconfig.json 23 ES2020, strict mode, CommonJS output
backend/src/index.ts 51 Express setup, middleware, route mounting, WA auto-init
backend/src/config/database.ts 41 PostgreSQL pool with connection string support
backend/src/services/WhatsAppService.ts 341 WA client singleton, message handling, media saving
backend/src/services/WebSocketService.ts 47 Socket.io singleton
backend/src/services/MediaService.ts 455 File org, thumbnails, transcripts, links
backend/src/controllers/statusController.ts 43 Connection state management
backend/src/controllers/chatController.ts 127 Chat list + contact details + DB archiving
backend/src/controllers/messageController.ts 124 Message history + text/media sending
backend/src/controllers/mediaController.ts 347 Media listing, sync, serve, delete, thumbnails
backend/src/routes/statusRoutes.ts 11 3 endpoints
backend/src/routes/chatRoutes.ts 10 2 endpoints
backend/src/routes/messageRoutes.ts 13 3 endpoints (1 with multer)
backend/src/routes/mediaRoutes.ts 14 7 endpoints

Frontend Files

File Lines Purpose
frontend/Dockerfile 39 Multi-stage: dev (Vite) + build + prod (nginx)
frontend/package.json 31 6 dependencies + 8 devDependencies
frontend/tsconfig.json ~24 Standard Vite React config
frontend/tsconfig.node.json 10 Vite config compilation
frontend/vite.config.ts 26 Proxy, aliases, source maps
frontend/tailwind.config.js 31 WhatsApp dark mode color palette
frontend/postcss.config.js 6 Tailwind + autoprefixer
frontend/index.html 13 Entry HTML with emoji favicon
frontend/src/main.tsx 10 React 18 createRoot
frontend/src/App.tsx 46 Router + layout structure
frontend/src/index.css 33 Tailwind + scrollbar styles
frontend/src/types/index.ts 105 10 TypeScript interfaces
frontend/src/services/api.ts 84 4 API modules, 14 methods
frontend/src/contexts/WhatsAppContext.tsx 233 Global state, 7 socket handlers, 8 actions
frontend/src/hooks/useSSE.ts ~40 SSE (EventSource) connection hook
frontend/src/pages/LoginPage.tsx 69 QR code + connection UI
frontend/src/pages/ChatPage.tsx 64 Chat sidebar + conversation layout
frontend/src/pages/SettingsPage.tsx 62 Settings cards
frontend/src/pages/MediaBrowserPage.tsx 835 Full media browser with all sub-components
frontend/src/components/TopBar.tsx 190 Status bar + navigation
frontend/src/components/ActionBar.tsx 209 Per-chat actions + sync result panel
frontend/src/components/ChatList.tsx 84 Searchable list with archive toggle
frontend/src/components/ChatListItem.tsx 76 Chat row with avatar, name, message, badge
frontend/src/components/ConversationView.tsx 55 Message area + header + composer
frontend/src/components/MessageBubble.tsx 48 Message display with ack ticks
frontend/src/components/MessageComposer.tsx 77 Text + file input + send
frontend/src/components/ContactProfile.tsx 163 Contact detail slide-over
frontend/src/components/QRCode.tsx 21 QR code display

Ports Summary

Service Port Protocol
Frontend (Vite) 3100 HTTP
Backend (Express) 3101 HTTP + WebSocket
PostgreSQL 5436 TCP

Key Lessons & Gotchas

  1. Express strict JSON mode rejects null — Always send {} not null as POST body with Content-Type: application/json
  2. lucide-react icon availability varies by version — Always verify an icon export exists before using it. Missing icon = entire React app crashes (blank page)
  3. Chromium needs shm_size: '1gb' in docker-compose — without this, Chromium crashes with shared memory errors
  4. wa_session_data Docker volume is critical — this persists the WhatsApp session. Without it, you need to re-scan QR on every container restart
  5. Stale Chromium lock files — after ungraceful shutdown, Puppeteer fails with “already running”. WhatsAppService cleans SingletonLock files on init
  6. WhatsApp video previews — available at msg._data.mediaData?.preview?._b64, not officially documented. Used as video thumbnails
  7. Vite proxy uses Docker service namehttp://wa-backend:3101 because frontend container resolves Docker DNS, not localhost
  8. useRef pattern for socket handlersactiveChatRef avoids stale closure issue where socket handlers capture old state