¶ apps/web
Next.js 14 frontend. Talks to the API through REST and receives live updates through Socket.IO.
Source: README.md
ReplyBox is a multi-tenant shared inbox SaaS for WhatsApp today, with room for future channels. The API owns correctness, the connector stays disposable, and every customer-facing state is anchored by organization-scoped data.
Next.js 14 frontend. Talks to the API through REST and receives live updates through Socket.IO.
FastAPI backend, REST source of truth, Socket.IO server, queue producer, RBAC gate, and internal webhook verifier.
Node.js Baileys WhatsApp Web adapter. Workers consume WhatsApp jobs and post signed events back to the API.
whatsapp
Data paths
REST uses httpOnly cookie JWT. Socket.IO reuses the access cookie during handshake.
WhatsApp mutations are BullMQ jobs on whatsapp. The Python producer is wire-compatible with the Node worker.
Internal events use HMAC-signed POST /internal/v1/... requests.
Socket.IO emits into one room per organization: org:{id}.
Architecture rule
There is exactly one connector-to-API transport. Extend the signed HTTP path when new connector events are needed; do not introduce parallel Redis pub/sub, connector websockets, or another hidden callback channel.
X-Replybox-Timestamp
HeaderX-Replybox-Signature
Payload{ts}\n{METHOD}\n{path}\n{body}
Verifierapps/api/app/security/hmac.py
Signerapps/baileys-connector/src/internal/api-client.ts
Every user-facing row carries organization_id, and public endpoints enforce membership. The pinned bearer-authenticated disciplinary provider routes are the documented server-to-server exception and expose no inbox API.
require_permission()
Seed source: apps/api/app/seeds/permissions.py
Auth uses httpOnly access and refresh JWT cookies. The web app refreshes through the axios interceptor in apps/web/lib/api.ts, and Socket.IO membership is re-validated on connect in apps/api/app/realtime/sio.py.
The WhatsApp channel lifecycle is deliberately small. Pairing is QR-only for now; pairing-code flow remains deferred.
disconnected
connecting
awaiting_qr
connected | failed | logged_out
Session and media storage
Baileys multi-file auth state is serialized, encrypted with CHANNEL_SESSION_KEY, and stored at channels/{id}/auth.bin. Media uses a separate MEDIA_OBJECT_KEY at channels/{id}/media/{message_id}.
Both domains use AES-256-GCM with the [iv(12) | tag(16) | ciphertext] layout. Keeping session and media keys separate limits blast radius if one key is exposed.
BullMQ and idempotency
connect
disconnect
send
send_media
send_location
download_media
resync_history
mark_read
group_subject_update
group_leave
send_reaction
Before sock.sendMessage, the worker checks Redis key send:done:{message_id} for a cached provider_message_id with a 24h TTL.
If a status webhook 5xxs and BullMQ retries the job, the cache hit re-posts the cached provider id instead of sending the WhatsApp message again.
send:{message_id}, download:{message_id}, mark_read:{conversation_id}:{last_message_id}, group_subject:{group_jid}, group_leave:{group_jid}, and react:{channel_id}:{target_provider_message_id}.
Realtime events
The API emits channel.qr, channel.status, channel.sync, message.created, and conversation.updated on org:{id}. Typed emitters live in apps/api/app/realtime/emit.py.
The web client folds realtime payloads into React Query caches. Do not trigger refetches on realtime events.
(channel_id, provider_message_id)
Broadcasts and protocol or poll updates are filtered before forwarding
Reactions and groups are first-class supported
Mapper: apps/baileys-connector/src/whatsapp/messages.ts
Local development
docker compose -f infra/docker/compose.yaml up -d
APIcd apps/api && python -m venv .venv && source .venv/bin/activate && pip install -e . && alembic upgrade head && uvicorn app.main:app --reload --port 8000
Webcd apps/web && pnpm install && pnpm dev
Connectorcd apps/baileys-connector && pnpm install && pnpm dev
Launcher./dev.sh
Database GUI
pgweb lives behind the Docker Compose tools profile and reads bookmarks from infra/docker/pgweb/bookmarks/*.toml. It never starts with normal infra.
docker compose -f infra/docker/compose.yaml --profile tools up -d pgweb
Openhttp://localhost:8085
Stopdocker compose -f infra/docker/compose.yaml --profile tools stop pgweb
Health checks
http://localhost:8000/healthz
Webhttp://localhost:3000/healthz
Connectorhttp://localhost:8787/health
The prod pgweb bookmark should use the read-only replybox_ro role, keep prod.toml gitignored, bind the tool on 127.0.0.1, and rely on --bookmarks-only so arbitrary connection strings cannot be entered.