End-to-end encryption is the official Signal library (@signalapp/libsignal-client: X3DH/PQXDH + Double Ratchet). The server only authenticates users, hands out public prekey bundles, and relays ciphertext — never plaintext, private keys, or history.
Keys live in the Electron main process, never in the UI. Identity is per-device; message history
is local, with an encrypted export/import to move it between devices.
Desktop only — libsignal can't run in a browser. No phones.
- uv (Python):
curl -LsSf https://astral.sh/uv/install.sh | sh - Node.js 20+
npm run setup # once: installs deps, creates server/.env with a fresh SECRET_KEY
npm run dev # starts the backend on :8000 (LAN-reachable) and opens a local app windowOther devices connect to this host's LAN IP. Get it with ipconfig getifaddr en0 (macOS).
Quick check from another device: open http://<host-ip>:8000/auth/config → {"iterations":...}.
(If unreachable: same Wi-Fi, and allow inbound TCP 8000 in the host firewall.)
Each device registers its own username — identity is per-device. Set the backend address in the Server field on the login screen (saved on the device, used for all traffic); it overrides any address baked into the build. Two ways to get the app:
A. Download the app (no terminal). On the host:
npm run build:app # auto-bakes this host's LAN IP, builds dist-electron/mac-arm64/Vault.appShare that Vault.app (AirDrop / USB / shared folder). Recipients on Apple-Silicon Macs open it
(first launch: right-click → Open, or xattr -dr com.apple.quarantine Vault.app). It's preconfigured
to reach the host. For Intel Macs / Windows / Linux, build on that OS or use option B.
B. Dev mode (repo + Node). Point the client at the host and run:
npm run setup # once
API_PROXY=http://<host-ip>:8000 npm run dev:client # opens the app, talks to the host- Register
aliceon one device,bobon another. alice→ New chat →bob→ send. First message runs X3DH/PQXDH; the rest ride the Double Ratchet.- Backup / move history: top-left
@usermenu → Export history (set a passphrase) → saves an encrypted file. On another device → Import history → same passphrase → conversations restored. - Proof it's E2EE:
sqlite3 server/e2ee.db "select envelope from delivery_queue"shows only ciphertext.
- Plain HTTP on a trusted LAN is fine: messages are E2EE regardless; only the login token and routing
metadata are visible on the wire. Don't expose
:8000to the internet without TLS. - Host IP changed? Just update the Server field on the login screen (saved per device) — no rebuild.
- Clean slate:
cd server && uv run python -m scripts.reset_db. If you reset the DB after devices have registered, also clear each device's identity (~/Library/Application Support/{Vault,e2ee-desktop}/stores/) and re-register, so its prekeys exist on the fresh server. - Tests:
npm run test:main·cd client && npm test·cd server && uv run pytest. - Design write-ups in
docs/(ARCHITECTURE.md,CRYPTO.md,THREAT-MODEL.md,DECISIONS.md). - Links AGPL-3.0
@signalapp/libsignal-client; the combined work is AGPL-3.0.
