Skip to content

NIKX-Tech/relayly

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

76 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Relayly

Relayly Logo

A self-hosted protocol and relay for end-to-end encrypted, device-to-device communication.

CI OpenSSF Scorecard CodeQL License
Go Version Latest Release Repo Size GitHub Stars Dependabot
Sponsor GitHub Sponsor Open Collective Go Reference npm PyPI Crates.io
Website Discord

Relayly is a protocol and reference relay for end-to-end encrypted, device-to-device communication over untrusted infrastructure. Devices authenticate to the relay and pair through a short out-of-band code; the session itself is established directly between the two devices using the Noise Protocol Framework (Noise_XX_25519_ChaChaPoly_BLAKE2s), giving mutual authentication and forward secrecy that hold regardless of the relay's own integrity. The relay coordinates pairing and forwards ciphertext, but holds no key material and is cryptographically excluded from every session it carries; operators run it themselves rather than depend on a third-party provider. See docs/PROTOCOL.md for the normative wire specification.


📖 Table of Contents


🧬 Protocol & Conformance

docs/PROTOCOL.md is the normative wire spec. Five independent SDKs (Go, TypeScript, Python, Rust, C++) implement it from scratch, and a required cross-language CI matrix pairs every one of them against every other, and against itself, before any change can merge. Conformance is defined the same way in the spec itself (§10): pass that matrix, or it doesn't count.

graph LR
    subgraph Devices["Your devices (pick any SDK)"]
        D1["📱 sdk/ts"]
        D2["💻 sdk/go"]
        D3["🛰️ sdk/cpp"]
    end

    D1 <-->|"Noise XX handshake<br/>E2E ciphertext only"| S
    D2 <-->|"Noise XX handshake<br/>E2E ciphertext only"| S
    D3 <-->|"Noise XX handshake<br/>E2E ciphertext only"| S

    S(["🔒 Relayly server<br/>authenticates + pairs devices<br/>holds no key material"])
Loading

The server and all five official SDKs (sdk/go, sdk/ts, sdk/py, sdk/rust, sdk/cpp) speak Protocol v1 today. See RFC-000 for the drift this replaced, and docs/ROADMAP.md for what's next. Protocol v1 links exactly one peer per device; multi-peer fan-out is scoped for v0.8.


✨ Features

Feature Detail
🔐 End-to-End Encryption Noise Protocol XX (X25519, ChaChaPoly) device-to-device; the relay holds no key material capable of reading messages
📱 Device Pairing 6-digit short code or QR code, no accounts required
Real-time Forwarding Low-latency WebSocket relaying with minimal server overhead
♻️ Auto-reconnect Exponential-backoff reconnection built into SDKs
🗄️ Zero-Config Storage Embedded SQLite storage, no external database required
🐳 Infrastructure Ready Pre-built Docker images and single portable binary
🖥️ Interactive Admin HTMX-powered dashboard for device and pairing management
🔑 Trustless Architecture Public Key Locking prevents server-side impersonation

⚙️ How it Works

Relayly authenticates devices, mediates pairing, and relays two kinds of WebSocket frames: JSON control messages (text) and an opaque E2E envelope (binary). The Noise XX handshake and all message encryption happen device-to-device; the relay forwards the binary envelope verbatim and holds no key material that could decrypt it.

sequenceDiagram
    participant A as Device A
    participant R as Relayly Server
    participant B as Device B

    Note over A,R: Connect + control channel (JSON text frames)
    A->>R: connect ?device_id&token
    R->>A: welcome
    A->>R: announce_key

    Note over A,B: Pairing (in-band 6-digit code, relayed by R)
    A->>R: pair_request
    R->>A: pair_code
    B->>R: pair_accept {code}
    R->>A: pair_complete
    R->>B: pair_complete

    Note over A,B: Noise XX handshake — device-to-device (binary envelope, relayed verbatim)
    B->>R: msg1
    R->>A: msg1
    A->>R: msg2
    R->>B: msg2
    B->>R: msg3
    R->>A: msg3

    Note over A,B: Transport (binary envelope, relayed verbatim — R never decrypts)
    A->>R: ciphertext
    R->>B: ciphertext, byte-identical
    B->>R: ciphertext
    R->>A: ciphertext, byte-identical
Loading

Encryption Details

Relayly runs Noise Protocol XX (Noise_XX_25519_ChaChaPoly_BLAKE2s) between the two paired devices. This provides:

  • Mutual Authentication: the two devices verify each other's static public keys directly with each other, the relay is not a party to the handshake.
  • Forward Secrecy: session keys are ephemeral; a fresh handshake runs on reconnect per the initiator rule in docs/PROTOCOL.md.
  • Zero-Knowledge Relay: the relay holds no cipher states and cannot decrypt the binary envelope; it only sees who is talking to whom and how much, not the content. Client-side key pinning (not just the relay's own announced-key check) is the real security boundary, see §7 of the spec.

Two layers of key locking guard against a compromised relay: devices pin the peer's static key on first pairing (the actual boundary), and the relay separately locks each device's announced key as defense in depth. See RFC-000 for how this design was chosen.


🚀 Quick Start

1. Server Setup (Docker)

The fastest way to get a relay running is via Docker:

git clone https://github.com/NIKX-Tech/relayly.git
cd relayly
docker compose up --build -d

# Want to test it? Try the Chat Demo (registers itself, no setup needed):
# cd examples/go/chat && go run .

🎮 Interactive Examples

Check out the examples/ directory for ready-to-run implementations:

Example Language Description
Chat Demo Go (Recommended) Live E2EE chat between two terminals
Clipboard Sync Go Sync clipboard across devices automatically
Basic Echo Go Simplest possible connect and message loop
Pair & Send Go CLI pairing and one-shot message exchange
Node.js Send TypeScript Connect, pair, and send from Node.js
Echo Server TypeScript Minimal echo client in TypeScript

2. Server Setup (Local)

# Build the binary (Requires Go 1.22+)
go build -o relayly ./cmd/relayly

# Start the server
./relayly start

# In another terminal, generate a pairing code
./relayly pair "My Phone"

📦 Official Client SDKs

Official SDKs for Go, TypeScript, Python, Rust, and C++ are in the sdk/ directory and published to their respective package registries (C++ is consumed via CMake FetchContent, see below).

Go SDK

go get github.com/NIKX-Tech/relayly/sdk/go
import relayly "github.com/NIKX-Tech/relayly/sdk/go"

key, _ := relayly.LoadOrGenerateKey("~/.relayly/device.key")

// deviceToken comes from POST /api/v1/devices (or `relayly pair`)
client, _ := relayly.Connect(ctx, "wss://your-server/ws", relayly.Options{
    DeviceID:    "my-laptop",
    DeviceToken: deviceToken,
    PrivateKey:  key,
})
defer client.Close()

code, _ := client.RequestPairCode(ctx)
fmt.Println("Code:", code.Short)

peer, _ := client.AcceptPair(ctx, "483921")
client.Send(ctx, peer.ID, []byte("hello!"))
msg := <-client.Messages()

pkg.go.dev/github.com/NIKX-Tech/relayly/sdk/go

TypeScript / JavaScript SDK

npm install relayly
import { RelaylyClient, generateKey } from 'relayly';

// deviceToken comes from POST /api/v1/devices
const client = new RelaylyClient('wss://your-server/ws', {
  deviceId: 'my-laptop',
  deviceToken,
  keyPair: generateKey(),
});

await client.connect();

const code = await client.requestPairCode();
console.log('Code:', code.shortCode);

const peer = await client.acceptPair('483921');
await client.send(peer.id, 'hello!');
client.on('message', (msg) => console.log(msg.payload));

npmjs.com/package/relayly - works in Node.js, browsers, and React Native.

Python SDK

pip install relayly
import asyncio, relayly

async def main():
    key = relayly.load_or_generate_key("~/.relayly/device.key")
    # device_token comes from POST /api/v1/devices
    client = await relayly.connect("wss://your-server/ws", relayly.Options(
        device_id="my-laptop",
        device_token=device_token,
        private_key=key,
    ))

    code = await client.request_pair_code()
    print("Code:", code.short)

    peer = await client.accept_pair("483921")
    await client.send(peer.id, b"hello!")
    async for msg in client.messages():
        print(msg.payload.decode())

asyncio.run(main())

pypi.org/project/relayly

Rust SDK

[dependencies]
relayly = "0.6"
tokio = { version = "1", features = ["full"] }
use relayly::{connect, load_or_generate_key, Options};
use std::path::Path;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let key = load_or_generate_key(Path::new("~/.relayly/device.key"))?;
    // device_token comes from POST /api/v1/devices
    let (client, mut messages) = connect("wss://your-server/ws", Options {
        device_id: "my-laptop".into(),
        device_token,
        private_key: key,
        ..Default::default()
    }).await?;

    tokio::spawn(async move {
        while let Some(msg) = messages.recv().await {
            println!("[{}] {}", msg.from, String::from_utf8_lossy(&msg.payload));
        }
    });

    let code = client.request_pair_code().await?;
    println!("Share this code: {}", code.short);
    let peer = code.wait().await?;
    client.send(&peer.id, b"hello!").await?;
    Ok(())
}

crates.io/crates/relayly

C++ SDK

include(FetchContent)
FetchContent_Declare(relayly
  GIT_REPOSITORY https://github.com/NIKX-Tech/relayly.git
  SOURCE_SUBDIR sdk/cpp
  GIT_TAG main
)
FetchContent_MakeAvailable(relayly)
target_link_libraries(your_target PRIVATE relayly::relayly)
#include <relayly/client.hpp>
#include <relayly/crypto.hpp>

using namespace relayly;

auto key = PrivateKey::LoadOrGenerate("~/.relayly/device.key");

Options opts;
opts.device_id = "my-laptop";
opts.device_token = device_token;  // from POST /api/v1/devices
opts.private_key = key;

auto client = Client::Connect("wss://your-server/ws", opts);

auto code = client->RequestPairCode();
std::cout << "Code: " << code.short_code() << "\n";

auto peer = client->AcceptPair("483921").get();
std::string hello = "hello!";
client->Send(peer.id, std::as_bytes(std::span(hello)));

See sdk/cpp/README.md for the threading model and dependency rationale.


💻 CLI Reference

Command Description
relayly start Start relay + admin servers
relayly start --config path/to/relayly.yaml Use custom config
relayly pair <name> Register device, print QR code
relayly pair <name> --no-qr Print token only
relayly link <id1> <id2> Pair two devices for relaying
relayly status Show connected devices + uptime
relayly status --format=json Machine-readable output

🔧 Configuration

All options can be set in config/relayly.yaml or via environment variables (RELAYLY_<KEY>, e.g. RELAYLY_PORT=9090):

Key Default Description
host 0.0.0.0 Listen address
port 8080 Relay WebSocket port
db.path ./data/relayly.db SQLite file
admin.enabled true Enable admin UI
admin.host 127.0.0.1 Admin bind address
admin.port 8081 Admin port
log.level info `debug
log.format json `json
tls.enabled false Enable TLS (or use reverse proxy)

🖥️ Admin UI

Visit http://localhost:8081 after starting the server.

  • Dashboard: Live connection count, uptime, device list.
  • Devices: Full device management with one-click revoke.
  • Auto-refreshes every 5 seconds via HTMX.

⚠️ The admin UI binds to 127.0.0.1 by default. Do not expose it publicly without authentication.


🔌 WebSocket Connection Protocol

This is a summary; docs/PROTOCOL.md is the normative spec.

Clients connect to: ws://<host>:<port>/ws?device_id=<uuid>&token=<device-token>

Auth happens at the HTTP layer (query params, before the WebSocket upgrade). There is no in-band auth frame and no client<->server cryptographic handshake.

Frame discipline

  • Text frames carry the JSON control channel: welcome, announce_key, pairing (pair_request/pair_code/pair_accept/pair_complete), peer_status, ping/pong, error.
  • Binary frames carry a 1-byte-prefixed E2E envelope (0x01 Noise handshake message, 0x02 Noise transport ciphertext) between the two paired devices. The relay forwards these verbatim, it does not parse, decrypt, or hold any key material for them.

Key locking

Two layers, both described in full in docs/PROTOCOL.md §7: each device pins its peer's static key on first pairing (the real security boundary), and the relay separately locks each device's announced key as defense in depth against a third party impersonating a device to the relay. Neither substitutes for the other.


🚢 Production Deployment

Recommended: Caddy as reverse proxy

relay.yourdomain.com {
    reverse_proxy localhost:8080
}

Security checklist

  • Run behind TLS (Caddy / nginx)
  • Bind admin UI to 127.0.0.1 (default)
  • Mount /data as a persistent volume (contains the database)
  • Back up /data/relayly.db

🛡️ Security & Privacy

Relayly is built on the principle of Privacy by Design:

  • Zero Data Harvesting: No accounts, emails, or tracking.
  • Public Key Locking: Once a device connects, the server "locks" it to that public key. Even a compromised server cannot swap keys without manual admin intervention.
  • Auditability: Small, dependency-light codebase written in memory-safe Go.

🏗 Project Architecture

relayly/
├── cmd/relayly/      # Main server entry point
├── internal/         # Private server logic (Relay, Database, Admin)
├── sdk/              # Official Client SDKs (Go, TS, Python, Rust, C++)
├── examples/         # Reference implementations
├── docs/             # Protocol specs & architecture deep-dives
├── .github/          # Unified CI/CD workflows
└── Dockerfile        # Optimized production image

💬 Community

Have questions or want to show off what you're building? Join our Discord Server to connect with other developers and get real-time support.


🤝 Contributing

Contributions are welcome! Please read our Contributing Guide for details on our code of conduct, and the process for submitting pull requests to us.


📄 License

Distributed under the MIT License. See LICENSE for more information.

© NIKX Technologies B.V.

About

Lightweight, self-hosted WebSocket protocol and reference relay for local-first applications. Enables secure end-to-end encrypted communication between your own devices using the Noise Protocol (XX handshake).

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Sponsor this project

Packages

Contributors

Languages