A self-hosted, WhatsApp-native AI assistant built on the Hermes Agent — with a multi-tenant web dashboard, Google Workspace tools, and allowlist-gated access control.
omniWA turns a WhatsApp account into an AI assistant you can talk to like a colleague: it remembers conversations per chat, manages your Google Calendar / Drive / Docs / Sheets / Gmail through natural language, runs scheduled jobs, and stays under your control through a web dashboard.
It is not a chatbot behind a webhook. WhatsApp transport is owned by a Hermes Agent native Baileys bridge running inside the same container as the agent brain. A FastAPI service acts purely as the control plane: dashboard auth, pairing, connection-mode switching, Google OAuth, permissions, and health — never in the message path.
- Two connection modes — pair the agent to your own number (self chat) or a dedicated bot number, and switch live from the dashboard (~1 min restart, pairing preserved)
- Allowlist access control — permit individual numbers, JIDs and group JIDs; DM/group policies (
allowlist/open/disabled) and a group @mention requirement are editable in the UI - Group Privacy Mode — Hermes plugin hooks inject a privacy directive into group turns and scrub emails, phone numbers and long numeric tokens out of group replies before delivery
- Per-chat persistent memory — one Hermes session per chat target, surviving restarts
- Google Workspace tools — Calendar, Drive, Docs, Sheets, Gmail via PKCE OAuth; refresh tokens encrypted per tenant in PostgreSQL
- Multi-tenant dashboard — email + password accounts (argon2id), Redis-backed sessions with instant revocation
- Native scheduling — proactive jobs run on Hermes cron, not a bolted-on scheduler
- Production posture — 5-service Docker Compose stack, health checks end-to-end (app / postgres / redis / bridge / hermes), ~150-test pytest suite
WhatsApp ⇄ Baileys bridge (inside hermes container, :8642)
│ allowlist · DM/group policy · @mention gate
▼
Hermes gateway ──── one session per chat target
│
Hermes Agent (ReAct loop, memory, cron)
├── tools: Google Calendar · Drive · Docs · Sheets · Gmail
└── reply delivered by the bridge itself
FastAPI backend (:8000) ── control plane, off the message path
├── dashboard UI + argon2 login + Redis sessions
├── WhatsApp pairing (QR) & disconnect
├── bridge configuration (mode/policies → shared volume + restart)
├── Google OAuth connect (per-tenant encrypted tokens)
├── permissions/trust management · preferences · API keys · health
└── owner notifications via bridge POST /send
| Service | Port | Role |
|---|---|---|
| hermes | 8642 | Nous Research Hermes Agent: gateway, OpenAI-compatible API, native Baileys bridge |
| backend | 8000 | FastAPI control plane: dashboard, pairing, OAuth, permissions, health |
| postgres | 5432 | Users, tenants, ACLs, encrypted Google tokens, audit logs |
| redis | 6379 | Dashboard sessions, caches |
| tunnel | — | Optional Cloudflare tunnel profile for a public URL |
The backend reads/writes the shared hermes_data volume (pairing session, runtime bridge config, soul/plugins) and can restart the hermes container over the Docker socket — that's how mode/policy changes apply without touching a terminal.
- Docker + Docker Compose
- A WhatsApp account to pair (QR scan)
- Google Cloud OAuth client (only if you want Workspace tools)
cp backend/.env.example backend/.env
# fill in at least:
# ADMIN_PASSWORD, SESSION_SECRET_KEY, OWNER_WA_PHONE,
# TOKEN_ENCRYPTION_KEY, GOOGLE_CLIENT_ID / _SECRETFor a public URL via Cloudflare Tunnel, put TUNNEL_TOKEN=... in docker/.env.
cd docker
docker compose up -d # core: postgres, redis, hermes, backend
docker compose --profile public up -d # add the tunnel when readycd backend
python -m scripts.seed_owner --email [email protected] --password 'a-strong-password'(Inside the deployment instead: docker exec -it <backend-container> python -m scripts.seed_owner ....)
- Open
http://localhost:8000/login(or your domain) and sign in. - WhatsApp tab → scan the QR with the phone you're pairing.
- Identity tab → pick Self Chat or Bot Number; set DM/group policies and the allowlist; hit Apply & Restart Agent.
- Permissions tab → grant contacts; Connect Google → finish OAuth.
Message the paired number and you're talking to your agent.
Key variables in backend/.env (see .env.example for the full annotated list):
| Variable | Purpose |
|---|---|
ADMIN_PASSWORD |
Legacy bootstrap admin password |
SESSION_SECRET_KEY |
Dashboard session signing key — change it |
OWNER_WA_PHONE |
Owner identity used by ACL/OAuth flows |
TOKEN_ENCRYPTION_KEY |
Fernet key encrypting Google tokens at rest |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
Workspace OAuth client |
POSTGRES_*, REDIS_* |
Data stores |
HERMES_BASE_URL, HERMES_API_KEY |
Backend ↔ Hermes API |
WHATSAPP_MODE, WHATSAPP_DM_POLICY, WHATSAPP_GROUP_POLICY, WHATSAPP_REQUIRE_MENTION, WHATSAPP_ALLOWED_USERS |
Bootstrap bridge config (runtime changes live on the shared volume via the dashboard) |
Runtime WhatsApp configuration lives on the hermes_data volume (bridge_env + policy block in config.yaml) and is managed from the dashboard — the env vars above only seed the first boot.
cd backend
./.venv/Scripts/python.exe -m pytest -q # Windows venv layoutCovers endpoints/auth, pairing + bridge config round-trips, permission cascade logic, group-privacy wiring, phone utilities and more (~150 tests against real Postgres + Redis test instances).
- Access is deny-by-default: with
dm_policy=allowlistonly explicitly permitted numbers/JIDs reach the agent. - Group Privacy Mode: two Hermes-side layers — a system-prompt directive for group turns plus deterministic redaction of outbound group replies; DMs untouched. Group streaming and tool-progress bubbles are disabled so partial output never leaks.
- Encrypted secrets: Google refresh tokens are Fernet-encrypted per tenant row; dashboard sessions are HttpOnly cookies backed by instantly-revocable Redis keys.
- No message transit through the backend: the control plane never sees chat content; it cannot read your conversations.
Tracked in CONTEXT.md ("Known Gaps & Future Work"). Notably: inbound per-sender rate limiting is not currently enforced (safe under a tight allowlist; revisit before opening DMs).
- CONTEXT.md — current architecture, key files, runtime configuration, known gaps
- docs/adr/ — architecture decision records (see
0007for the v3 transport redesign) - DATABASE_SCHEMA.md — PostgreSQL schema
- AGENTS.md — repo conventions for coding agents
- hermes-plugin/ — the agent's soul prompt + group-privacy plugin source