Skip to main content

relayly

Python SDK for Relayly - a self-hosted, end-to-end encrypted WebSocket relay for local-first apps.

Async-first (asyncio). Encryption is device-to-device Noise XX (Noise_XX_25519_ChaChaPoly_BLAKE2s) via noiseprotocol (see "Why noiseprotocol?" below); the relay itself holds no key material. See docs/PROTOCOL.md for the full wire spec.

Install

pip install relayly

Quick start

import asyncio
import 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://relay.example.com/ws", relayly.Options(
        device_id="my-laptop",
        device_token=device_token,
        private_key=key,
    ))

    async for msg in client.messages():
        print(f"[{msg.from_device}]", msg.payload.decode())

asyncio.run(main())

Pairing

v1 links exactly one peer per device. Pairing again replaces whatever was linked before, it doesn't add a second one alongside it. Multi-peer support is a roadmap item (docs/ROADMAP.md, v0.8). Don't build for N simultaneous peers against this version.

Devices pair using a short 6-digit code shared out-of-band (or via QR). Both accept_pair() and code.wait() block until the Noise handshake actually completes (not just until the code exchange), so the peer they resolve with is immediately safe to send() to.

# Device A - request a code
code = await client.request_pair_code()
print("Share this code:", code.short)
print("QR URL:", code.qr_code_url("wss://relay.example.com"))

peer = await code.wait()  # blocks until the other device pairs
print("Paired with", peer.id)

# Device B - accept the code
peer = await client.accept_pair("483921")

Peer key pinning

Each peer's authenticated static key is pinned on first pairing and checked on every handshake after — this pin, not the relay, is the real security boundary (docs/PROTOCOL.md §7). By default it's stored at ~/.relayly/peers.json, the same schema every other official SDK reads/writes, so a shared machine can keep one pin store across languages:

client = await relayly.connect(url, relayly.Options(
    device_id=device_id,
    device_token=device_token,
    private_key=key,
    peer_store_path="~/.relayly/peers.json",  # this is the default
))

A peer presenting a different key than its pin raises PeerKeyMismatchError — this is never auto-retried; unpinning is an explicit action (remove the entry from the store, or use relayly.PeerStore directly).

Sending messages

await client.send(peer.id, b"hello!")
await client.send(peer.id, "hello!".encode())

send() raises NotReadyError if the peer's session isn't up yet — in normal use this only happens briefly after a reconnect forces a re-handshake; use on_ready to know when it recovers.

Reconnection

The client reconnects automatically with exponential backoff, and re-runs the Noise handshake per docs/PROTOCOL.md §6 (the device with the lexicographically smaller ID re-initiates; the existing session keeps working until the replacement completes):

relayly.Options(
    device_id="my-laptop",
    device_token=device_token,
    private_key=key,
    reconnect_delay=2.0,       # initial delay in seconds (default: 1.0)
    max_reconnect_delay=30.0,  # backoff ceiling (default: 60.0)
    on_disconnect=lambda err: print("disconnected:", err),
    on_reconnect=lambda: print("reconnected"),
    on_ready=lambda peer_id: print("session ready with", peer_id),
    on_peer_status=lambda peer_id, online: print(peer_id, "online:", online),
)

Set reconnect_delay=-1 to disable automatic reconnection.

Key management

# Generate a fresh key
key = relayly.generate_key()

# Save and load manually
key.save_to_file("~/.relayly/device.key")
key = relayly.load_key_from_file("~/.relayly/device.key")

# Load or generate in one call (recommended)
key = relayly.load_or_generate_key("~/.relayly/device.key")

Options

Option Type Default Description
device_id str - Unique ID for this device. Required.
device_token str - From POST /api/v1/devices. Required.
private_key PrivateKey - X25519 private key. Required.
peer_store_path str ~/.relayly/peers.json Pinned peer key storage path.
ping_interval float 30.0 Keepalive ping interval (seconds).
reconnect_delay float 1.0 Initial reconnect delay. Set to -1 to disable.
max_reconnect_delay float 60.0 Backoff ceiling (seconds).
on_disconnect Callable None Called with the exception when connection drops.
on_reconnect Callable None Called after a successful reconnect.
on_ready Callable None Called whenever a peer's session becomes usable for send().
on_peer_status Callable None Called on the paired peer's online/offline transitions.

Why noiseprotocol?

docs/PROTOCOL.md requires Noise_XX_25519_ChaChaPoly_BLAKE2s. Unlike TypeScript (where no maintained library fit and the state machine had to be hand-written), Python has noiseprotocol (PyPI, import name noise), which supports this exact suite by name (NoiseConnection.from_name(b'Noise_XX_25519_ChaChaPoly_BLAKE2s')) and delegates its DH/cipher/hash backends to cryptography — an actively maintained, audited library. The Noise-pattern orchestration code in noiseprotocol itself hasn't been released since 2020, but it's a thin, near-literal transcription of the spec sitting on top of cryptography's primitives, which is exactly where the real risk would be. This SDK's use of it is verified byte-for-byte against flynn/noise (the Go implementation already used server-side and in sdk/go) using fixed keys and a deterministic random source, not just "the library claims to support it."

Requirements

  • Python 3.11+
  • websockets >= 12.0
  • noiseprotocol >= 0.3.1
  • cryptography >= 41.0.0

License

MIT

Metadata

Release files for relayly 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for relayly 0.6.0
File Size Uploaded Provenance Actions
relayly-0.6.0.tar.gz 25.0 kB Without provenance Details for relayly-0.6.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for relayly 0.6.0
File Interpreter ABI Platform Actions
relayly-0.6.0-py3-none-any.whl Python 3 none any Details for relayly-0.6.0-py3-none-any.whl

Total release size: 44.8 kB

Release files / relayly-0.6.0.tar.gz

Download URL relayly-0.6.0.tar.gz
Size 25.0 kB
Tags Source
SHA-256 checksum
How to use checksums
f5b7607d306dbeb3b55fd390b6d872999ac5d84548133e3b5bed43457e57ecbb
BLAKE2b-256 checksum
How to use checksums
a7c94f2de570983271672e9c9bee4514a34fd8507af34acb9c577deb0dfa7486
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.13

Release files / relayly-0.6.0-py3-none-any.whl

Download URL relayly-0.6.0-py3-none-any.whl
Size 19.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ce4f67e07f223c7908c32e257fcd80585bce9e24ed485446406690d491c9f7f3
BLAKE2b-256 checksum
How to use checksums
3086955e1b8eb4271764a83825a39dfb88a0b10311a3c9daed3f3ff339c8a775
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.13

Security for relayly 0.6.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page