Skip to content

End-to-End Encrypted Chat — Client-Side Encryption, Stateless Relay, No External Dependencies

GitHub Repository

Steps to Self-host the Relay Server on Railway

Purpose

A minimal, end-to-end encrypted group chat using a dumb relay server. Clients send encrypted messages to the relay, which forwards them to intended recipients without ever seeing plaintext or private keys. This repository includes:

  • A Relay server implemented in Java using WebSocketServer (hosted by one member of the group).
  • A simple client UI in HTML/JS that connects to the relay (can be hosted or run locally).

The goal is to demonstrate a simple group chat where clients handle all encryption locally and do not rely on external services, providers or dependencies like signal, whatsapp, or telegram. This ensures all the chat data remains private between participants.


Scenario where this is useful

You want to have a private group chat with friends, family, or colleagues without relying on third-party platforms.

How it works:

  1. One person (the admin) runs the relay server and shares the invite token with the group.
  2. Each participant opens the client UI in their browser, enters the invite token and WebSocket URL, and connects to the relay.

Now you have a private group chat open, where all messages are end-to-end encrypted on the client side. The relay simply forwards encrypted messages without ever having access to the plaintext or keys.


How it works

The diagram below shows the full lifecycle of a session — from a client joining, to sending encrypted messages, to disconnecting.

sequenceDiagram
    participant Client as Your Browser
    participant Relay as Relay Server
    participant Peers as Other Participants

    Note left of Client: 1. Generate a keypair locally (never sent to server)
    Client->>Relay: Join — send nickname, public key, invite token
    Relay-->>Client: Confirm join — send list of connected peers
    Relay-->>Peers: Notify everyone a new peer joined

    Note left of Client: 2. Sending a message
    Client->>Client: Encrypt message for each peer using their public key
    Client->>Relay: Send encrypted blob (ciphertext + nonce) to target peer
    Relay->>Peers: Forward the encrypted blob — relay never sees plaintext
    Peers-->>Client: Receive blob, decrypt using own private key

    Note left of Client: 3. Leaving the session
    Client->>Relay: Connection closed
    Relay-->>Peers: Notify everyone this peer left

Joining the session

When a client connects, the browser generates a fresh X25519 keypair using tweetnacl. The private key never leaves the device. The client sends only its public key, nickname, and invite token to the relay. The server validates the invite token, checks the user cap, and responds with a roster of everyone currently connected — including their public keys — so the joining client can start encrypting messages for each of them.

Sending a message

When you send a message, the browser encrypts it separately for each peer using that peer's public key and your private key (NaCl box, XSalsa20-Poly1305). Each encrypted blob — just a ciphertext and a nonce — is sent to the relay, which forwards it to the right recipient. The relay only ever sees opaque Base64-encoded blobs; it has no way to read the content.

The server applies a few guardrails before forwarding: it rate-limits messages, enforces a 64 KB size cap, and rejects any message missing required fields (publicKey, ciphertext, nonce).

Disconnecting

When a client disconnects, the relay removes them from the session and notifies remaining peers. If the admin disconnects, a new admin is automatically elected from whoever is still connected.


Setup & Usage Guide

For the Admin (Running the Relay Server)

The admin is responsible for hosting the relay server and sharing the invite token with participants out-of-band.

flowchart LR
    Admin[Admin] -->|starts server| Server[Relay Server]
    Server -->|generates & prints token| Token[ Share Invite Token with Participants ]

Note: a relay server is just another server that forwards encrypted messages to connected clients.
Steps:

  1. Choose a host machine (a cloud VPS, big cloud provider, etc.)
  2. Clone the repo and navigate to the java-relay-server directory.
  3. Ensure Java (JDK) and Maven are installed.
  4. Inside relay.java, adjust MAX_USERS if needed (default is 5).
  5. Build the server: mvn clean package from the java-relay-server directory.
  6. Start the server: java -jar target/relay-server-1.0.0.jar
  7. Share the invite token printed in the server logs with participants.

For Participants (Connecting as a Client)

Each participant opens the client UI in their browser and connects using the invite token and WebSocket URL provided by the admin.

flowchart LR
    Clients -->|enter token and WebSocket URL in UI| Connect[Connected]

If using Python:

  1. Change to the ui directory.
  2. Run: python3 -m http.server 3000
  3. Open http://localhost:3000 in your browser.

If using Node:

  1. Change to the ui directory.
  2. Install live-server: npm install -g live-server
  3. Run: live-server --port=3000
  4. Open http://localhost:3000 in your browser.

Once the UI is open, enter your nickname, the WebSocket URL (e.g. wss://website.com:8080), and the invite token to connect.


Dependencies

Component Dependencies
Server Java (JDK) + Maven; Java WebSocket implementation + gson (managed via Maven)
Client tweetnacl and tweetnacl-util for browser-side crypto (load from CDN or bundle with a Node build)
Local serving Python (python3 -m http.server) or Node (live-server)

Deploying with Railway (https://railway.com)

We will deploy the relay server on Railway, which provides a simple way to host web applications. The client UI will be served locally.

Steps to deploy the relay server:

  1. Create a new project on Railway and connect your GitHub repository. Once you select the repository, this will take you to deployment settings.

    Alt text

  2. Source section in deployment settings: change source to /java-relay-server

    Alt text

  3. Networking section in deployment settings: enable public networking by clicking generate domain.

  4. Build section in deployment settings: set the build command to mvn clean package

    Alt text

  5. Deploy section in deployment settings: set the start command to java -jar target/relay-server-1.0.0.jar

    Alt text

  6. Press deploy and wait for the deployment to complete.

  7. After deployment, view logs (from deployment tab) to find the invite token printed by the server. Share this token with participants.

    In the settings (networking) you will get the public domain for your server, which will be used as the WebSocket URL in the client UI (e.g. your-project.up.railway.app, when using this url, make sure to add the wss:// prefix, which becomes wss://your-project.up.railway.app).

Opening the UI: Follow the steps in the For Participants (Connecting as a Client) section above to serve the client UI locally, and open http://localhost:3000 in your browser. Add your name, the WebSocket URL from Railway (make sure to add the wss:// prefix), and the invite token to connect to the relay server.

Alt text

And Voila! You should now be connected to the relay server hosted on Railway, and can start chatting securely with other participants.

Alt text

Top left is your public key, and under each participant's name is their public key. This can be used for out-of-band verification to ensure you're communicating with the intended peers.


Things to look out for when using.

  • Compare public keys in the user interface with out-of-band verification methods to confirm authenticity of the users you're communicating with.
  • Ensure the relay runs on HTTPS/WSS; this is the operator's responsibility.
  • The relay maintains user connections and stores associated public keys - something to be mindful of.
  • Adjust server logs according to privacy requirements.
  • Forward secrecy note: As long as clients and the server are not compromised, this provides forward secrecy — past sessions remain private; compromise during an active session still exposes that session.
  • There is a message limit of 64 KB.
  • Update max users in relay.java.
  • The way this is currently set up, each relay server will simulate one chat room.
  • If you want to reset the invite token, simply restart the relay server. This will disconnect all clients and generate a new token for the next session.

Future improvements

  1. Persist keys: Use IndexedDB so identity survives refresh on the browser.
  2. Logging: Avoid printing INVITE_TOKEN to public logs; show it only to the admin securely.
  3. Password protection: Add a password on the login page as an additional security measure.
  4. Group encryption: Implement a more efficient group encryption scheme (e.g. Signal's Sender Keys) to avoid encrypting separately for each peer.