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

VariableDefaultDescription
RELAYLY_PORT8080WebSocket and admin UI port
RELAYLY_DB_PATHdata/relayly.dbSQLite database file path
RELAYLY_ADMIN_ENABLEDtrueEnable the HTMX admin dashboard
RELAYLY_ADMIN_HOST127.0.0.1Interface the admin UI binds to
RELAYLY_LOG_LEVELinfoLog 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.1 by default. It is not exposed to the network unless you explicitly change RELAYLY_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.