Elephant
A privacy-first, self-hostable messenger. Signal Protocol end-to-end encryption, a Go backend and a Flutter app.
- Status
- Active · v0.3.2 (November 2026)
- Role
- Started the project; wrote most of the Go server and much of the Flutter app. Built with Chandru and Manideep, with app fixes from Hamdan.
- Stack
- Go · Flutter · PostgreSQL 16 · Signal Protocol · Docker
- Links
- Source · Releases · Docker Hub
Problem #
Most messengers ask you to trust the server. I wanted one where the server can’t read direct messages, that anyone can run on their own hardware, and whose code is open (AGPL-3.0). The goal was a real system, not a demo. That meant real-time delivery, working offline, sane auth, and releases people can install.
Approach #
Elephant is three tiers: a Flutter client, a Go server and PostgreSQL 16.
- Direct messages are end-to-end encrypted with the Signal Protocol. X3DH runs on the
device to agree keys, and the Double Ratchet runs there to encrypt each message, using
libsignal_protocol_dart. The server only stores public prekey bundles and opaque ciphertext. - Contact verification. You can mark a contact’s identity key as verified. The mark resets automatically if that key ever changes.
- One WebSocket per client carries chat, typing indicators, read receipts and presence. REST handles everything else: auth, discovery and history.
- Offline-first client. The app keeps a SQLCipher-encrypted local database and queues outgoing messages while offline. It replays them on reconnect.
- Self-hostable. The server ships as a Docker image. The app can point at any Elephant server from the login screen.
Architecture #
Flutter app ──HTTPS──> chi router + middleware
│ (CORS, rate limit, logging, JWT)
│ │
│ ▼
│ controllers ─> services ─> repository
│ ▲ │
└─────WSS───> WebSocket hub ────────┘ │ pgx pool
(goroutine + buffered ▼
send channel per PostgreSQL 16
connection)
The server follows a strict layering: routes/, middlewares/, controllers/, services/,
repository/, models/. Only the repository layer talks to the database.
The WebSocket hub is the core of the design. Each connection gets its own goroutine and its own buffered send channel. When a message arrives, the hub looks up the recipient’s connection and writes to that channel. A writer goroutine drains it. A slow or stalled client can only back up its own channel. It can’t block the hub or anyone else.
On the server side I built the key-distribution API: uploading identity keys and prekeys, fetching a prekey bundle, and recording or resetting verification. Chandru built the client-side Signal integration on top of it.
Trade-offs #
- Goroutine per connection. A goroutine starts with a few kilobytes of stack, and the scheduler parks blocked ones cheaply. Thousands of idle clients cost little, so I took the simple model over an event loop.
- Argon2id cost vs. login latency. Memory, iterations and parallelism are configurable, because stronger hashes make every login slower. Expensive hashing is also a denial-of-service lever: an unthrottled login endpoint lets anyone burn server CPU and memory. The rate limiter currently guards user search; putting it in front of the auth endpoints is next.
- Group messages are not end-to-end encrypted yet. Pairwise Signal sessions don’t scale cleanly to groups, which need something like Sender Keys or MLS. I’d rather ship encrypted DMs and say so plainly than claim more than is there.
- Short-lived JWTs with rotating refresh tokens. This adds client complexity, but a leaked access token stops being useful quickly.
- Plain HTTP/WS from the server. TLS terminates at a reverse proxy (Caddy or nginx). That keeps the binary simple and leaves certificates to the tools built for them.
Results #
- Eight tagged releases since July 2026 (v0.1.5 to v0.3.2). A version tag builds server images for Docker Hub and GHCR and attaches Android APKs to the release, as a universal build plus per-ABI builds.
- The server image has been pulled 1,929 times from Docker Hub (refreshed daily).
- The backend CI runs the Go test suite against a real PostgreSQL 16 service container. It covers auth integration, middleware and controllers.
- 12 SQL migrations, applied in order by a small script that records each one, so re-running is safe.
- F-Droid packaging is submitted, and the merge request is open.
Status and next steps #
Elephant is actively developed. Next up: rate limiting on the auth endpoints, end-to-end encryption for group chats, and getting the F-Droid listing merged.