Self-hosted encrypted messaging for teams and individuals.
Whispr first emerged as a BILSEM graduation project. It is a project that I have been working on for many years, and as a result of the emergence of AI, I have made debugging and a few improvements with AI. I have now decided to make this project public, but this is still a project in development, community contributions are welcome.
| Component | Language | Purpose |
|---|---|---|
| Server | Go | WebSocket + REST API, SQLite persistence |
| Client | Flutter | Cross-platform UI (Linux, Android) |
- End-to-end encrypted messaging with Signal and optional PGP direct messages
- Real-time WebSocket communication
- Group chats with per-member encryption
- Self-hosted — your data stays on your server
- Single binary deployment (~10MB)
- Independently switchable Modern and Retro interfaces
cd server
go build -o whispr-server
JWT_SECRET=0123456789abcdef0123456789abcdef ./whispr-servercd client
flutter pub get
flutter run -d linux # Desktop
flutter run -d android # MobileNormal client builds use the safe https://localhost:8080 default until the
user selects another server.
Environment variables for the server:
| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
Server port |
DATABASE_PATH |
whispr.db |
SQLite database path. Existing whisper.db is reused automatically when present. |
JWT_SECRET |
(generated) | JWT signing secret |
TOTP_ENCRYPTION_KEY |
(generated) | Separate key for encrypting TOTP seeds; back up .totp_secret when generated |
JWT_ACCESS_TTL |
24h |
Access token lifetime |
JWT_REFRESH_TTL |
4320h |
Refresh token lifetime |
HOST |
127.0.0.1 |
Listen address; keep loopback behind a TLS proxy |
ALLOWED_ORIGINS |
http://localhost:* |
Browser CORS/WebSocket origins |
TRUSTED_PROXIES |
127.0.0.1,::1 |
Proxies allowed to supply forwarded client IPs |
LOG_FILE |
empty | Optional JSON application log path. Leave empty to avoid app log files and use journalctl. |
UPLOAD_DIR |
uploads |
Local upload storage path |
ATTACHMENT_STORAGE_LIMIT_MB |
360 |
Max encrypted attachment storage; full stores reject uploads without deleting history |
FCM_CREDENTIALS_FILE |
unset | Firebase service-account JSON for Android push notifications |
See deploy/systemd/README.md for systemd paths, environment, logs, and the
user-operated deployment workflow.
The isolated release builder produces signed Linux x86_64 and Android arm64
artifacts without installing Flutter, Java, or an Android SDK on the host. It
requires external Android and OpenPGP signing keys and never falls back to debug
signing. See release/README.md.
Official artifacts are signed by the pinned 0xleitfader release identity. See
release/SIGNING.md before trusting a downloaded build.
POST /api/v1/auth/register - Create account
POST /api/v1/auth/login - Login
POST /api/v1/auth/refresh - Refresh token
POST /api/v1/auth/logout - Logout
GET /api/v1/channels - List channels
POST /api/v1/channels - Create channel
POST /api/v1/channels/direct - Find/create DM
GET /api/v1/channels/{id}/members - List members
POST /api/v1/channels/{id}/members - Add member
POST /api/v1/channels/{id}/transfer-ownership - Transfer group ownership
POST /api/v1/channels/{id}/leave - Leave a group
GET /api/v1/channels/{id}/messages - Message history
POST /api/v1/attachments - Upload encrypted attachment bytes (`channel_id` required)
POST /api/v1/keys/signal-identity - Publish a Signal device identity
GET /api/v1/keys/prekey-bundle - Claim a device prekey bundle
POST /api/v1/keys/pgp-public-key - Publish an optional PGP public key
WS /ws
Authenticate WebSocket connections with the `Sec-WebSocket-Protocol` header:
`bearer, <JWT>` or `bearer.<JWT>`. Do not put access tokens in URLs.
- Direct Signal messages use X3DH/PQXDH prekeys and Double Ratchet sessions, with ciphertext fan-out to each registered recipient device.
- Group messages use Signal SenderKey. Distribution messages are themselves Signal-encrypted per recipient device; the server stores only ciphertext.
- PGP is available as an optional direct-message mode. Private keys and local message/media caches are encrypted at rest with a platform-keystore key.
For remote deployments, terminate TLS with a reverse proxy, set
PUBLIC_BASE_URL=https://chat.example.com, and never expose plaintext port
8080 to the internet.
MIT