Introduction
Relayly is a lightweight, self-hosted WebSocket relay for local-first, end-to-end encrypted device communication.
It enables trustless message routing between your own devices (phone, laptop, desktop) through a server you control. All communication is encrypted using the Noise Protocol, ensuring the relay server only ever handles opaque cryptographic blobs.
Why Relayly?
Most relay and tunneling tools require you to trust a third-party server with your data, or require accounts and cloud infrastructure. Relayly is different:
- Zero accounts: devices are identified by cryptographic keys, not emails
- Zero plaintext: the relay forwards encrypted frames it cannot read
- Zero cloud: runs on your Raspberry Pi, VPS, or even a local machine
- Zero lock-in: MIT licensed, portable single binary
When should I use Relayly?
Relayly is ideal for:
- Local-first applications that need device sync
- IoT deployments in environments with unreliable internet
- Privacy-sensitive device communication (health, finance, personal data)
- Development environments where you want to route between devices without ngrok/cloud
- Networks with censorship or intermittent connectivity
Architecture
relayly/
├── cmd/relayly/ # Main server entry point
├── internal/ # Private server logic (Relay, Database, Admin)
├── sdk/ # Official Client SDKs (Go, TypeScript)
├── examples/ # Reference implementations
├── docs/ # Protocol specs
└── Dockerfile # Production image Next Steps
- Quick Start: Get a relay running in under 60 seconds
- Installation: Docker or binary installation
- Configuration: Full configuration reference
- SDKs: Go and TypeScript client libraries
Quick Start for Developers
Start the server
docker compose up --build -d
docker exec relayly /relayly pair "My Device"
# ✓ Pairing code: 483 921 | Expires in 5 min Connect with the Go SDK
import "github.com/NIKX-Tech/relayly/sdk/go/relayly"
client, err := relayly.NewClient(relayly.Config{
ServerURL: "ws://localhost:8080/ws",
PairingCode: "483921",
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Send a message to another paired device
err = client.Send(ctx, peer.ID, []byte("Hello from Go!"))
// Receive messages
client.OnMessage(func(msg *relayly.Message) {
fmt.Printf("From %s: %s\n", msg.From, msg.Data)
}) Full reference: pkg.go.dev/github.com/NIKX-Tech/relayly/sdk/go/relayly
Connect with the TypeScript SDK
import { RelaylyClient } from "@nikx/relayly";
const client = await RelaylyClient.connect({
serverUrl: "ws://localhost:8080/ws",
pairingCode: "483921",
});
// Send a message
await client.send(peer.id, new TextEncoder().encode("Hello from TS!"));
// Receive messages
client.onMessage((msg) => {
console.log(`From ${msg.from}:`, new TextDecoder().decode(msg.data));
}); Full reference: npmjs.com/package/@nikx/relayly
Self-Host in 5 Minutes
Prerequisites
- Docker (recommended): any version supporting Compose V2, or
- Go 1.24+: for building the binary directly
Option A: Docker Compose (recommended)
# Clone the repository
git clone https://github.com/NIKX-Tech/relayly.git
cd relayly
# Start in the background
docker compose up --build -d
# Register your first device (returns a pairing code)
docker exec relayly /relayly pair "My Laptop" The relay will be available at ws://localhost:8080/ws and the admin dashboard at http://localhost:8080/admin (localhost-only by default).
Option B: Go binary
git clone https://github.com/NIKX-Tech/relayly.git
cd relayly
# Build
go build -o relayly ./cmd/relayly
# Start
./relayly start Configuration
The server reads config/relayly.yaml on startup, and every key can be overridden with a RELAYLY_* environment variable.
| Variable | Default | Description |
|---|---|---|
RELAYLY_PORT | 8080 | WebSocket and admin UI port |
RELAYLY_DB_PATH | data/relayly.db | SQLite database file path |
RELAYLY_ADMIN_ENABLED | true | Enable the HTMX admin dashboard |
RELAYLY_ADMIN_HOST | 127.0.0.1 | Interface the admin UI binds to |
RELAYLY_LOG_LEVEL | info | Log verbosity (debug, info, warn, error) |
Security notes
- Pairing codes expire in 5 minutes. There are no long-lived shared secrets, devices authenticate via cryptographic public keys after the initial pairing.
- The admin UI binds to
127.0.0.1by default. It is not exposed to the network unless you explicitly changeRELAYLY_ADMIN_HOST. - The relay never sees plaintext. All message content is end-to-end encrypted by the client SDKs using the Noise Protocol XX before transmission.