When you look at existing self-hosted messengers, you usually see one of two things: either complex infrastructure that’s hard to deploy (Matrix/Synapse), or minimalism with no encryption. ONYX is an attempt to find the middle ground: easy to deploy, real E2E encryption, and the ability to work entirely in a local network without internet at all.
Project architecture
| Component | Technology |
|---|---|
| Client | Flutter (Android, Windows, macOS, Linux) |
| Server | Node.js — Express + express-ws + ws |
| Database | MariaDB + Redis (sessions, cache) |
| File storage | S3-compatible (AWS SDK v3) |
| Transport | WebSocket (wss://) + HTTP/REST |
| Encryption | X25519 + XChaCha20-Poly1305 + AES-256-GCM |
LAN mode: works without internet
One of the key features of ONYX is the ability to communicate in a local network without internet at all. A custom device auto-discovery mechanism handles this entirely.
Discovery protocol via UDP broadcast
Every client broadcasts a JSON packet to 255.255.255.255:45678 every 5 seconds:
{
"username": "alice",
"timestamp": 1710000000000,
"pubkey": "<X25519 public key, base64>"
}
All other clients listen on that port and update two local tables:
username → source IP addressusername → X25519 public key
No mDNS, no manual IP entry — just pure UDP broadcast. The public key is included directly in the broadcast packet, so encrypted communication can start immediately without an additional handshake.
Media transfer in LAN
Media files go through a separate channel on port 45679, split into ~32 KB chunks. Each chunk is encrypted independently with AES-256-GCM, which allows decryption and rendering to begin before the full file is received.
Encryption: two layers on elliptic curves
No RSA anywhere in the project — only a modern elliptic curve stack.
E2EE scheme for private chats
- Key exchange: X25519 ECDH with an ephemeral key pair per session
- Key derivation: HKDF-SHA256 with a context label (
onyx-lan-v2for LAN, separate labels for E2EE chats) - Encryption: XChaCha20-Poly1305 AEAD
Packet format:
[pubkey 32B] [nonce 12B] [ciphertext] [mac 16B]
Why XChaCha20-Poly1305 and not AES-GCM?
AES-GCM requires hardware acceleration (AES-NI) for decent performance. XChaCha20-Poly1305 runs in constant time on any hardware — important for mobile devices without AES-NI. It also has a wider nonce (192 bits vs 96 for GCM), which reduces collision risk in long sessions.
AES-256-GCM is used for LAN media transfers — chunked delivery, and hardware acceleration is available on most desktops.
Multi-device and E2EE
When a new device connects to an account, it sends an authorization request to a trusted device. The trusted device must explicitly approve the new one — only then does the key exchange happen. The server never has access to decrypted content, even when adding a new device.
Multi-device sync
When a message arrives, the server sends it to each of your devices separately — encrypted with that device’s specific public key. Technically these are different encrypted messages for each device, just with the same plaintext inside.
One honest limitation: only incoming messages sync across devices. Outgoing messages are visible only on the device they were sent from. Full bidirectional sync with E2EE requires either a “copy to self” encryption mechanism or server-side plaintext storage — both are worse tradeoffs.
Why Flutter and not Electron or native development
The requirement from day one: one codebase for Windows, macOS, Linux and Android. Three options were considered:
| Option | Problem |
|---|---|
| Native development | 3–5 separate codebases, constant desync |
| Electron | +150–200 MB RAM, DOM rendering |
| Flutter | Single codebase, Skia/Impeller, real 60fps |
Flutter Desktop required writing 10+ separate optimization modules (fps_booster, fps_optimizer, fps_stimulator, message_load_optimizer, chat_preloader) — Flutter on desktop lags noticeably without tuning. But the result is smooth UI across all four platforms from one repo.
Desktop-specific integrations
- System tray — app minimizes to tray instead of closing.
- Single-instance — prevents multiple copies via IPC. A second launch focuses the existing window.
- Custom titlebar — system titlebar hidden, custom header with drag zone
- Windows-native notifications — separate module, not Flutter overlay
- Autostart on system boot
Security beyond E2EE
- PIN + biometrics — Face ID / fingerprint via Flutter Secure Storage
- Proxy support —
proxy_manager.dart, routing through any proxy - Secure storage — all sensitive data through OS secure storage (Keychain / Android Keystore)
- Active session management — all connected devices visible, any session can be terminated remotely — but only from a trusted device
Self-hosted groups and channels
Two types of groups and channels in ONYX — fundamentally different models.
Built-in (via ONYX server)
Standard groups and channels work through the central ONYX server and are not encrypted — a deliberate tradeoff for reliable sync. Suitable for open communities where E2EE is not a requirement.
External (self-hosted)
Anyone can run their own instance — on a VPS, home server, or local network:
| Use case | Description |
|---|---|
| Local network | File sharing and chat within office/home network, no internet |
| Private community | Closed group on your VPS, join by invite |
| Public channel | You host it, subscribers read posts |
- Group — two-way, all participants can write
- Channel — one-way broadcast, only admins publish
Connect to any external server directly from the app — enter the instance address and join.
Deploying your own instance
Server software — ONYX Server: github.com/wardcore-dev/onyx-server
Favorites: local notes and storage
ONYX has a dedicated Favorites tab — not a “Saved Messages” clone, but a proper local notebook. Create any number of favorite chats, each with its own avatar and name, as separate categories: passwords, ideas, saved media, links.
Everything is stored locally on the device — nothing sent to the server, nothing synced. The server knows nothing about your favorites.
Accounts: anonymity, multi-account and deletion
- Registration — username and password only. No phone number, no email.
- Username — chosen once, permanent. Can never be changed. You can change your display name, but not the username itself.
- Multi-account — register and hold any number of accounts, switch freely.
- Account deletion — delete at any time with all media and server-side data. No traces left.
Current state
Project is in working beta. Development is ongoing. Happy to answer questions in the comments.


Not sure if you broke out of the Matrix here. OP’s reply contained an em-dash, started with an affirmation. Follows the rule of three. I’d say there’s still a high likelihood it’s an AI which “claims” the code went through review.
Fair skepticism, but no - I used AI for the English translation of my post, since I’m not a native speaker.
The future of validating if people are “real” or not is beginning to feel like fighting with ghosts.
But I feel you, using github copilot as spicy autocomplete is the only way it is useful to me too. I also sort-of like the AI summaries of messages in my notification bar. I just wish that was all guaranteed to be locally generated. I’m kind of hoping a budget NPU will make a decent open local spicy-autocomplete solution at some point.
On the internet, no one knows I’m a horse.
In the future, I’d delete these…LOL. It’s like touching off a power keg in here. But, using AI for language translation is perfectly fine for me.