About this project

Wappie is a multi-tenant WhatsApp API server with a sealed archive, plus a web client that consumes the same API. It connects business systems to WhatsApp over HTTP and WebSocket, with shared workspaces, per-number permissions and a messaging client. The server, CLI, web client and basic administration are Apache-2.0 open source; managed hosting and commercial billing are maintained separately, the hosted pilot is invitation-only and free, and payments there are simulated. Functionally, the server pairs a WhatsApp device from the terminal using either an eight-character code (typed under Linked devices) or a QR rendered in the terminal, then seals that device's traffic on the way in. It ingests history sync, projects edits, revocations and reactions, records receipts, and tracks which revision each reader had on screen. Inbound media is stored exactly as the Meta CDN served it, and outbound media upload and send are supported, as is view-once. Structured content (location, poll, contact, event), contacts, names and profile pictures, on-demand backfill and a conversation layer with unread counts, ticks, presence, groups and polls are all implemented. Access is through a CLI tool (`wsctl`) and an HTTP/WebSocket API, with a hosted endpoint and `/v1/ws` for WebSocket. The archive protection model is the project's centrepiece. WhatsApp media arrives already AES-256-CBC encrypted with an encrypt-then-MAC HMAC under a 32-byte media key; the ciphertext is stored verbatim and only the media key is sealed to a device public key. Message bodies are sealed with HPKE (RFC 9180, X25519 + HKDF-SHA256 + AES-256-GCM) under a content key covering a batch, which the README justifies on cost grounds rather than throughput. The server holds only public keys: it can seal and cannot open. Each device has an archive keypair generated by whichever client paired it; the private half is sealed to the public key of every account that may read that device (a key grant) and then forgotten. Account private keys are generated in the browser at signup, wrapped under an Argon2id-derived key bound to the account address, and never transmitted; a recovery code wraps the same key a second time. The browser performs HPKE, content-key handling, message bodies, contact names, profile pictures and attachment decryption inside the page. Permissions are layered: API keys carry a scope of `read`, `send` or `full`; a member reaches only devices granted to them; an owner or admin reaches every device's envelope and can pair, grant, mint keys and toggle a device between discreet and loud; no scope reaches tenant configuration. A third party is given a service account — a keypair with no password, granted devices like a person and reached through an API key acting as it. Retention is off by default; a tenant can set a window that the server applies hourly to messages, receipts, group events and attachments, while chats and contacts stay. A person can be erased from the archive across every device, and deleting a device or resetting an archive removes its attachment objects from storage. Logs mask identifiers at every level. Running locally needs PostgreSQL 18 or newer for `uuidv7()`, and a non-superuser role because superusers bypass row-level security. Object storage is optional: with none configured, attachments queue in the database until storage appears. A `make dev-up` target brings up Postgres and MinIO in Docker. Test targets cover formatting, vetting, layout, race-enabled tests, the browser client's typecheck and build, coverage and fuzzing, and each test runs in its own Postgres schema. The web client is built with Vite into `web/dist` and served by `WS_WEB_DIR`; nothing is embedded in the Go binary, and with no build present the server serves the API only. During development the Vite server proxies `/v1` to the Go port, because the websocket handler accepts same-origin connections only. The README states the limits plainly rather than implying more than is delivered. An attacker running code on a live server sees plaintext in flight between Signal decryption, sealing and storage, so at-rest sealing protects a stolen disk, a leaked backup or a database dump, not a compromised process. The whatsmeow session store must remain readable by the process; whoever steals it can impersonate the device and read new messages, but not the archive. Outbound text passes through in the clear, and outbound media too, because WhatsApp's upload accepts cleartext only; inbound media is never decrypted server-side. Attachment metadata (type, size, dimensions, duration, hashes) and routing metadata, including receipts, are readable, so a database dump reveals the social graph and who read what and when, but not content. Revoking a grant stops a key being obtained again but cannot recall an already-unlocked copy, since the key lived in a browser. Losing every access path loses the archive permanently for everyone. Browser session material sits in IndexedDB as ciphertext under non-extractable WebCrypto keys, and the client ships a content security policy and loads no third-party JavaScript. The status table lists completed phases from skeleton, migrations and media crypto through pairing, ingest, edit/revoke/react projection, media inbound and outbound, history sync, media retry, contacts, on-demand backfill, the web client, per-device keys and accounts, the conversation layer, incognito and quotas, and a security audit pass; the admin console is listed as next. Cross-implementation test vectors are generated in Go and opened by both implementations, including negative cases such as a blob moved to another row or presented under another kind, so the browser client cannot silently agree only with itself. A `seeddemo` tool writes a small fake conversation through the real ingest pipeline for developing the client without pairing a phone.