ONYX: self-hosted messenger with LAN mode and E2EE — an indie project story
from wardcore@lemmy.world to selfhosted@lemmy.world on 23 Mar 13:58
https://lemmy.world/post/44633944

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:

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

  1. Key exchange: X25519 ECDH with an ephemeral key pair per session
  2. Key derivation: HKDF-SHA256 with a context label (onyx-lan-v2 for LAN, separate labels for E2EE chats)
  3. 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


Security beyond E2EE


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

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


Current state

Project is in working beta. Development is ongoing. Happy to answer questions in the comments.

Try it out

#selfhosted

threaded - newest

Strit@lemmy.linuxuserspace.show on 23 Mar 14:18 next collapse

You should probably disclaim that this was built with the help of Claude…

From the .gitignore file:

/.claude
wardcore@lemmy.world on 23 Mar 14:21 collapse

Fair point! Yes, Claude was used as a coding assistant throughout the project. That said, every single line went through strict manual review — nothing was blindly copy-pasted into the codebase. All architectural decisions, the crypto stack choices, and the overall design are my own. Claude helped with boilerplate and speeding things up, but the project is not “vibe-coded”.

Strit@lemmy.linuxuserspace.show on 23 Mar 14:28 collapse

Gald to hear someone using it the way it should be used. As an assistant.

hendrik@palaver.p3x.de on 23 Mar 15:10 collapse

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.

wardcore@lemmy.world on 23 Mar 15:14 collapse

Fair skepticism, but no - I used AI for the English translation of my post, since I’m not a native speaker.

greyscale@lemmy.grey.ooo on 23 Mar 15:21 next collapse

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.

irmadlad@lemmy.world on 23 Mar 17:16 collapse

The future of validating if people are “real” or not is beginning to feel like fighting with ghosts.

On the internet, no one knows I’m a horse.

irmadlad@lemmy.world on 23 Mar 17:15 collapse

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.

yardratianSoma@lemmy.ca on 23 Mar 14:50 next collapse

seems cool, as I certainly seen this rivalry between complexity and simplicity. I don’t really need E2EE for a simple private communication server, but it is nice to have. I mean, We use it when relying on 3rd party services, because we don’t want them spying on our communications, but when it’s a private server hosted locally/in the cloud, the only unwanted eyes are hackers, who I don’t think care that much about private servers with small numbers of users.

wardcore@lemmy.world on 23 Mar 15:01 collapse

Just to clarify — E2EE in ONYX is only for private chats. Groups and channels (both built-in and self-hosted) don’t have E2EE, which is actually closer to your point — for groups it’s a deliberate tradeoff for simplicity and reliable sync. So you’re right, for that use case TLS is enough.

warmaster@lemmy.world on 23 Mar 15:58 collapse

If this has voice channels like Discord, it would be perfect for LAN parties and exactly what I wanted Mumble to become.

wardcore@lemmy.world on 23 Mar 16:53 collapse

that’s a great idea, I’ll consider adding it in one of the upcoming updates.