Lightweight, self-hosted WebSocket relay for local-first, end-to-end encrypted device communication.
Relayly enables trustless message routing between your own devices (phone, laptop, desktop, etc.) through a server you control. All communication is encrypted using the Noise Protocol, ensuring the relay server only ever handles opaque cryptographic blobs.
- Features
- How it Works
- Quick Start
- Official Client SDKs
- CLI Reference
- Configuration
- Admin UI
- WebSocket Connection Protocol
- Production Deployment
- Security & Privacy
- Contributing
| Feature | Detail |
|---|---|
| 🔐 End-to-End Encryption | Noise Protocol XX (X25519, ChaChaPoly), server never sees plaintext |
| 📱 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 |
Relayly acts as a "dumb" router that facilitates secure handshakes and message forwarding.
sequenceDiagram
participant A as Device A (Initiator)
participant R as Relayly Server
participant B as Device B (Responder)
Note over A,B: 1. Noise XX Handshake
A->>R: Handshake Message 1 (Ephemeral Pubkey)
R->>B: Forward Handshake 1
B->>R: Handshake Message 2 (Encrypted Static + Ephemeral)
R->>A: Forward Handshake 2
A->>R: Handshake Message 3 (Encrypted Static)
R->>B: Forward Handshake 3
Note over A,B: 2. E2EE Tunnel Established
A->>R: Encrypted Payload
R->>B: Forwarded Payload
B->>R: Encrypted Response
R->>A: Forwarded Response
Relayly uses Noise Protocol XX for the initial handshake and subsequent message transport. This provides:
- Mutual Authentication: Both devices verify each other's static public keys.
- Forward Secrecy: Session keys are ephemeral and discarded after use.
- Zero-Knowledge Relay: The server handles zero plaintext data.
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
# Register your first device
docker exec relayly /relayly pair "My Device"
# Want to test it? Try the Chat Demo:
# cd examples/go/chat && ./setup.shCheck 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 |
# 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 SDKs for Go, TypeScript, and Python are in the sdk/ directory and published to their respective package registries.
go get github.com/NIKX-Tech/relayly/sdk/goimport relayly "github.com/NIKX-Tech/relayly/sdk/go"
key, _ := relayly.LoadOrGenerateKey("~/.relayly/device.key")
client, _ := relayly.Connect(ctx, "wss://your-server/ws", relayly.Options{
DeviceID: "my-laptop",
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
npm install relaylyimport { RelaylyClient, generateKey } from 'relayly';
const client = new RelaylyClient('wss://your-server', {
deviceId: 'my-laptop',
keyPair: generateKey(),
});
await client.connect();
client.on('message', (msg) => console.log(msg.payload));npmjs.com/package/relayly - works in Node.js, browsers, and React Native.
pip install relaylyimport asyncio, relayly
async def main():
key = relayly.load_or_generate_key("~/.relayly/device.key")
client = await relayly.connect("wss://your-server", relayly.Options(
device_id="my-laptop",
private_key=key,
))
async for msg in client.messages():
print(msg.payload.decode())
asyncio.run(main())[dependencies]
relayly = "0.3"
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"))?;
let (client, mut messages) = connect("wss://your-server", Options {
device_id: "my-laptop".into(),
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(())
}| 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 |
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 |
noise.key_path |
./data/server.noise.key |
Server Noise keypair |
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) |
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 to127.0.0.1by default. Do not expose it publicly without authentication.
Clients connect to:
ws://<host>:<port>/ws?device_id=<uuid>&token=<pair-token>
- Client → Server: [msg1: ephemeral pubkey]
- Server → Client: [msg2: encrypted server static + ephemeral]
- Client → Server: [msg3: encrypted client static]
After handshake, all subsequent frames are opaque encrypted binary, the relay never inspects them.
relay.yourdomain.com {
reverse_proxy localhost:8080
}- Run behind TLS (Caddy / nginx)
- Bind admin UI to
127.0.0.1(default) - Mount
/dataas a persistent volume (contains DB + keypair) - Back up
/data/relayly.dband/data/server.noise.key
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.
relayly/
├── cmd/relayly/ # Main server entry point
├── internal/ # Private server logic (Relay, Database, Admin)
├── sdk/ # Official Client SDKs (Go, TS)
├── examples/ # Reference implementations
├── docs/ # Protocol specs & architecture deep-dives
├── .github/ # Unified CI/CD workflows
└── Dockerfile # Optimized production image
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.
Contributions are welcome! Please read our Contributing Guide for details on our code of conduct, and the process for submitting pull requests to us.
Distributed under the MIT License. See LICENSE for more information.
