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 address
  • username → 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

  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

  • 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 supportproxy_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.

Try it out

  • warmaster@lemmy.world
    link
    fedilink
    English
    arrow-up
    1
    ·
    7 hours ago

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

    • wardcore@lemmy.worldOP
      link
      fedilink
      English
      arrow-up
      3
      arrow-down
      1
      ·
      6 hours ago

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