A zero-knowledge WebSocket relay server for end-to-end encrypted peer connections by Olib AI
Used in StealthOS — The privacy-focused operating environment.
StealthRelay is a self-hosted Rust relay server that routes WebSocket messages between ConnectionPool peers without ever seeing their content. All application data is end-to-end encrypted using a Noise NK handshake (dual X25519 DH) with ChaCha20-Poly1305 session ciphers — the server handles only opaque blobs.
The relay operates on a zero-knowledge model: deploy it on your own infrastructure, claim ownership via a one-time QR code, and invite friends through cryptographically signed invitation URLs. The server authenticates hosts with Ed25519 signatures and protects against abuse with adaptive proof-of-work, per-IP rate limiting, and progressive blocking.
Five Rust crates, #![forbid(unsafe_code)] on all of them, 166 unit tests, and 30 E2E tests.
One-line install (Linux / macOS / Raspberry Pi):
curl -fsSL https://raw.githubusercontent.com/Olib-AI/StealthRelay/main/scripts/install.sh | bashWindows (run PowerShell as Administrator):
irm https://raw.githubusercontent.com/Olib-AI/StealthRelay/main/scripts/install.ps1 | iexThe installer downloads a pre-built binary, verifies its SHA256 checksum, creates a system service, and starts the server. No Docker, no dependencies.
Prefer Docker? See Docker Compose or One-Click Cloud Deploy below.
After installation, a setup page opens automatically in your browser:
http://localhost:9091/setup?token=<TOKEN>
The setup page displays a QR code — scan it with the StealthOS app to claim ownership. You can also copy the manual code from the page.
Security: The setup URL includes a one-time token that is only printed to the server console. Without this token, the page returns 403 Forbidden — even if someone can reach port 9091.
Headless / Raspberry Pi? Open the setup URL from any device on your local network. The installer prints the LAN-accessible URL to the terminal.
Remote server (SSH)? The setup page binds to
127.0.0.1:9091and isn't accessible remotely. Use SSH port forwarding to access it from your local browser:ssh -L 9091:localhost:9091 user@your-serverThen open
http://localhost:9091/setup?token=<TOKEN>in your local browser. The token is printed in the server logs — check withsudo journalctl -u stealth-relay | grep setup.
Open StealthOS → Connection Pool → Host Remote Pool:
- Enter your server URL (
ws://your-ip:9090) - Tap Scan QR Code and point your camera at the setup page
- Or tap Enter Code Manually and paste the code
The server is now bound to your device. A recovery key is shown once — save it securely. It is the only way to reclaim the server if you lose your device.
Once claimed, tap "Invite a Friend" in the pool lobby. Share the generated link or QR code.
When friends open the invitation:
- The server forwards their join request to you
- You approve or reject from your device
- Any connected member can also create invite links — but you always approve
Chat, play games, share files — all end-to-end encrypted. The server routes messages but can never read them.
sequenceDiagram
participant Host as You - Host
participant Server as Your Server
participant Friends as Friends
Host->>Server: 1. Install (one-liner)
Host->>Server: 2. Scan QR from setup page
Note over Server: Server claimed
Host->>Server: 3. Create invitation
Server->>Friends: 4. Share link/QR
Friends->>Server: Join request
Server->>Host: 5. Approve join
Host->>Server: Approved
Host--)Friends: E2E encrypted messages
Friends--)Host: E2E encrypted messages
Note over Server: Server sees only opaque blobs
- Noise NK handshake — Dual X25519 Diffie-Hellman with HKDF-SHA256 key derivation for forward-secret session establishment
- ChaCha20-Poly1305 session cipher — Authenticated encryption for all application data with automatic symmetric ratchet at 2^20 messages
- Ed25519 host authentication — Domain-separated timestamp signatures verified by the relay before pool creation
- HMAC invitation tokens — 256-bit tokens with HKDF derivation and constant-time comparison; one-time use, time-limited, host approval required
- Server claiming — One-time 256-bit QR code visible only in Docker logs; per-IP rate-limited with progressive blocking and recovery key fallback
- Host key integrity — Identity key files use a v2 format with 4-byte magic (
STKY), 32-byte seed, and 32-byte BLAKE2b-256 MAC; corruption or tampering is detected on load - Session tokens — 32-byte server-issued tokens required for all privileged host operations and guest
Forwardframes (constant-time comparison) - Display name sanitization — Control characters, newlines, and excessive length (>64 chars) stripped before logging or storage
- Adaptive proof-of-work — SHA-256 hashcash with difficulty that scales from 18-bit (~50ms) to 26-bit (~13s) based on request rate
- Per-IP rate limiting — Connection rate, message rate, and failed authentication tracking with IPv6 /48 normalization
- Progressive blocking — IPs exceeding limits are blocked for configurable durations (default 10 minutes)
- JSON depth bomb defense — Recursive nesting in
SessionResumedframes is structurally prevented by using flatBufferedRelayedMessagetypes - 64 KB message cap — Oversized WebSocket frames are rejected before processing
- Connection registry cleanup — Connections are properly unregistered on disconnect, preventing
active_countdrift from blocking new connections
- Docker-first deployment — Multi-stage build producing a minimal Debian image with no shell
- Cloudflare Tunnel support — Production deployment via
cloudflaredsidecar for TLS termination without managing certificates - Native TLS — Optional
rustlswith SPKI SHA-256 pin verification for direct TLS termination - Recovery key — One-time recovery key issued at claim time for server rebinding if the host device is lost
- Graceful shutdown — Signal handling with connection draining
- Mutex poison recovery — All
claim_statemutex accesses recover from poison (viaPoisonError::into_inner) instead of panicking, preventing a single thread panic from killing the server Arc<str>broadcast — Broadcast messages share oneArc<str>allocation across all recipients instead of cloning per-peer, reducing allocation pressure for large pools
- Structured logging — JSON or pretty output via
tracingwith per-crate filter directives - Prometheus metrics — Connection counts, pool counts, message rates exposed on a dedicated metrics port
- Health endpoint — JSON health status with built-in Docker
HEALTHCHECK
graph TD
subgraph Workspace["StealthRelay Workspace"]
Server["stealthos-server\nCLI binary, TOML config loading\nWebSocket message handler\nserver claiming, join approval routing"]
Core["stealthos-core\nPool registry, peer routing\ninvitation lifecycle, rate limiting\nserver frame types"]
Crypto["stealthos-crypto\nEd25519 identity, X25519 key exchange\nChaCha20-Poly1305, HKDF\nPoW challenge/verify"]
Transport["stealthos-transport\nWebSocket listener, connection actors\nbackpressure, rustls TLS termination\nconnection registry"]
Observability["stealthos-observability\nStructured logging via tracing\nPrometheus metrics exporter\nhealth/readiness HTTP endpoints"]
Server --> Core
Core --> Crypto
Crypto --> Transport
Transport --> Observability
end
Security is not bolted on — it is structural. Every layer enforces its own guarantees.
The host authenticates to the relay by signing pool_id || timestamp || nonce with a Keychain-stored Ed25519 private key. The relay verifies the signature against the bound host public key before creating a pool. On each new WebSocket connection, the server issues a per-connection nonce via an auth_challenge frame. The host must include this nonce in the signed transcript, binding the authentication to that specific connection. Timestamps are checked within a 30-second skew window. Clients that omit the nonce or provide a mismatched nonce are rejected.
Invitation tokens are 256-bit secrets derived via HKDF. The relay stores only the HMAC commitment — never the raw token. Tokens are:
- One-time use (or configurable
max_uses) - Time-limited (configurable
expires_in_secs) - Host-approved — the relay forwards join requests to the host for explicit approval
- Ed25519-signed — invitation URLs include a signature binding the token to the server address and pool ID
Joining peers must solve a SHA-256 hashcash challenge before their request is forwarded to the host. Difficulty scales adaptively:
| Requests/min | Difficulty | Expected Hashes | Solve Time |
|---|---|---|---|
| 0-50 | 18 bits | ~262k | ~50ms |
| 51-200 | 22 bits | ~4M | ~800ms |
| 201+ | 26 bits | ~67M | ~13s |
Challenges include a timestamp to prevent pre-computation and are verified within a time window.
| Limit | Default | Scope |
|---|---|---|
| Connection rate | 30/min | Per-IP (IPv6 /48 normalized) |
| Message rate | 60/sec | Per-connection |
| Failed auth | 5 attempts | Per-IP, then blocked |
| Block duration | 10 min | Per-IP |
After successful HostAuth, the server issues a 32-byte session token for the host. All privileged host operations require this token:
CreateInvitationRevokeInvitationJoinApprovalKickPeerClosePool- Host-originated
Forward
Guest peers also receive a session token at join-approval time. The server stores each guest's token and validates it on every Forward frame. All token comparisons use constant-time equality (subtle::ConstantTimeEq) to prevent timing side-channels.
The handshake follows the Noise NK pattern where the client knows the server's static X25519 public key (obtained via invitation or out-of-band):
sequenceDiagram
participant Client
participant Server
Note over Client: Generate ephemeral X25519 keypair
Note over Client: shared_es = X25519(client_ephemeral_sk, server_static_pk)
Client->>Server: client_ephemeral_pk, client_ed25519_pk, timestamp, signature
Note over Server: Generate ephemeral X25519 keypair
Note over Server: shared_ee = X25519(server_ephemeral_sk, client_ephemeral_pk)
Server->>Client: server_ephemeral_pk, timestamp, signature
Note over Client,Server: ikm = shared_es || shared_ee
Note over Client,Server: session_keys = HKDF-SHA256(ikm, salt, info, 96)
This provides forward secrecy via ephemeral keys, server authentication via the static key (NK pattern), and client authentication via Ed25519 signature over the handshake transcript. All ephemeral secrets are zeroized after key derivation.
All display names received from clients are sanitized before logging or storage: control characters and newlines are stripped, and length is truncated to 64 characters. This prevents log injection and terminal escape attacks.
| Data | Visible to Relay? | Notes |
|---|---|---|
| Connected peer IDs | Yes | Required for routing |
| Peer IP addresses | Yes | Required for TCP connections |
| Message routing metadata | Yes | Who sends to whom, when, message type |
| Message sizes and timing | Yes | Inherent in transport |
| Pool membership | Yes | Required for pool management |
| Message content | No | E2E encrypted (ChaCha20-Poly1305) |
| Chat text | No | Inside encrypted payload |
| Game moves | No | Inside encrypted payload |
| Shared files | No | Inside encrypted payload |
| Pool codes | No | Only HMAC commitments stored |
| Invitation secrets | No | Only HMAC commitments stored |
- Non-root user (
stealthos) - Read-only filesystem
- All Linux capabilities dropped (
--cap-drop ALL) - No shell in the image
no-new-privilegessecurity option- 256 MB memory limit
- tmpfs for
/tmpwithnoexec,nosuid
The fastest way to self-host on your own hardware — a laptop, desktop, or single-board computer like a Raspberry Pi.
Linux / macOS:
curl -fsSL https://raw.githubusercontent.com/Olib-AI/StealthRelay/main/scripts/install.sh | bashWindows (PowerShell as Administrator):
irm https://raw.githubusercontent.com/Olib-AI/StealthRelay/main/scripts/install.ps1 | iexThe installer:
- Downloads the correct binary for your OS and architecture (amd64 / arm64)
- Verifies the SHA256 checksum against the signed release
- Creates a system service (systemd on Linux, launchd on macOS, Windows Service)
- Opens the setup page in your browser for one-click claiming
Supported platforms: Ubuntu, Debian, Fedora, RHEL, Arch, Alpine, Raspberry Pi OS, macOS (Intel & Apple Silicon), Windows 10/11.
| Option | Description |
|---|---|
--version v1.0.0 |
Install a specific version |
--update |
Update binary, preserve keys and config |
--uninstall |
Remove everything (prompts before deleting keys) |
--no-service |
Don't create a system service |
--no-browser |
Don't auto-open the browser |
Deploy StealthRelay to your preferred cloud provider:
All providers terminate TLS at the edge — no certificate management needed.
The recommended self-hosted deployment method:
docker compose -f docker/docker-compose.yml up -d
docker compose -f docker/docker-compose.yml logs -f stealth-relayThis provides:
- Persistent key volume (
stealth-keys) - Read-only config bind-mount
- Security hardening (read-only FS, no capabilities, no new privileges)
- Resource limits (256 MB memory, 2 CPUs)
- Health checks every 30 seconds
- Log rotation (10 MB max, 3 files)
For internet access without opening ports or managing TLS certificates:
# 1. Create a tunnel at https://one.dash.cloudflare.com → Networks → Tunnels
# 2. Set CLOUDFLARED_TOKEN in docker/.env
# 3. Deploy with the overlay compose file
docker compose -f docker/docker-compose.yml \
-f docker/docker-compose.cloudflared.yml up -dThe overlay removes the public WebSocket port binding — all traffic flows through the tunnel. Your relay will be available at wss://your-tunnel-hostname.
For direct TLS termination without Cloudflare, configure rustls with your certificate and key files. ConnectionPool clients support SPKI SHA-256 pin verification via a custom URLSessionDelegate for certificate pinning.
All configuration values can be overridden with environment variables using the STEALTH_ prefix and double-underscore nesting:
| Environment Variable | Compose Variable | Description |
|---|---|---|
STEALTH_SERVER__WS_BIND |
— | WebSocket bind address |
STEALTH_SERVER__METRICS_BIND |
— | Metrics bind address |
STEALTH_LOGGING__LEVEL |
STEALTH_LOG_LEVEL |
Log filter directive |
STEALTH_LOGGING__FORMAT |
STEALTH_LOG_FORMAT |
Output format (json or pretty) |
STEALTH_CRYPTO__KEY_DIR |
— | Host identity key directory |
| — | STEALTH_WS_PORT |
Host-side WebSocket port mapping |
| — | STEALTH_METRICS_PORT |
Host-side metrics port mapping |
| — | CLOUDFLARED_TOKEN |
Cloudflare Tunnel token |
Config precedence: environment variables (STEALTH_ prefix) > TOML file > defaults.
See config/default.toml for the annotated configuration file.
| Setting | Default | Description |
|---|---|---|
server.ws_bind |
0.0.0.0:9090 |
Address the WebSocket listener binds to |
server.metrics_bind |
127.0.0.1:9091 |
Address for the internal health/metrics HTTP endpoint |
server.max_connections |
500 |
Maximum number of concurrent WebSocket connections |
server.max_message_size |
65536 |
Maximum size of a single WebSocket message in bytes (64 KiB) |
server.idle_timeout |
600 |
Seconds of inactivity before a connection is closed (10 min) |
server.handshake_timeout |
10 |
Seconds allowed for a client to complete the WebSocket handshake |
| Setting | Default | Description |
|---|---|---|
pool.max_pools |
100 |
Maximum number of active pools on this relay |
pool.max_pool_size |
16 |
Maximum peers allowed in a single pool |
pool.pool_idle_timeout |
300 |
Seconds of pool inactivity before automatic cleanup (5 min) |
| Setting | Default | Description |
|---|---|---|
crypto.key_dir |
/var/stealth-relay/keys |
Directory for host identity key files (Ed25519 seed + X25519 static key) |
crypto.auto_generate_keys |
true |
Automatically generate a host keypair on first start if none exists |
| Setting | Default | Description |
|---|---|---|
logging.level |
info |
Tracing filter directive (e.g., info, stealthos_server=debug, stealthos_server=trace,tower=warn) |
logging.format |
json |
Output format: json for production (log aggregators), pretty for development |
| Setting | Default | Description |
|---|---|---|
rate_limit.connections_per_minute |
30 |
Maximum new connections per IP per minute |
rate_limit.messages_per_second |
60 |
Maximum messages per connection per second |
rate_limit.max_failed_auth |
5 |
Maximum failed authentication attempts per IP before temporary block |
rate_limit.block_duration_secs |
600 |
Duration in seconds an IP stays blocked after exceeding limits (10 min) |
All frames are JSON-encoded with an internally-tagged frame_type discriminator.
| Frame | Fields | Description |
|---|---|---|
host_auth |
host_public_key, timestamp, signature, pool_id, nonce, server_url?, display_name? |
Authenticate as the pool host with Ed25519 signature (nonce from auth_challenge) |
join_request |
token_id, proof, timestamp, nonce, client_public_key, display_name, pow_solution? |
Request to join a pool with an invitation token |
forward |
data, target_peer_ids?, sequence, session_token? |
Forward opaque E2E encrypted data to peer(s) |
create_invitation |
max_uses, expires_in_secs, session_token? |
Host creates an invitation token |
revoke_invitation |
token_id, session_token? |
Host revokes an existing invitation |
join_approval |
client_public_key, approved, reason?, session_token? |
Host approves or rejects a pending join request |
kick_peer |
peer_id, reason, session_token? |
Host kicks a peer from the pool |
close_pool |
session_token? |
Host closes the pool |
handshake_init |
client_ephemeral_pk, client_identity_pk, timestamp, signature |
Noise NK handshake step 1 |
claim_server |
claim_secret, host_public_key, display_name |
Claim an unclaimed server |
reclaim_server |
recovery_key, new_host_public_key, display_name |
Reclaim a server using the recovery key |
ack |
sequence |
Acknowledge receipt of a sequence number |
heartbeat_ping |
timestamp |
Client heartbeat |
| Frame | Fields | Description |
|---|---|---|
auth_challenge |
nonce |
Server-issued per-connection nonce for HostAuth replay protection |
server_hello |
server_ephemeral_pk, server_identity_pk, pow_challenge?, timestamp, signature |
Noise NK handshake step 2 with optional PoW challenge |
host_auth_success |
pool_id, session_token |
Pool created, session token issued |
join_accepted |
session_token, peer_id, peers[], pool_info |
Join approved with current pool state |
join_rejected |
reason |
Join request denied |
join_request_for_host |
client_public_key, token_id, proof, timestamp, nonce, display_name |
Forwarded join request for host approval |
invitation_created |
token_id, url, expires_at |
Invitation token generated |
peer_joined |
peer |
A new peer joined the pool |
peer_left |
peer_id, reason |
A peer left the pool |
relayed |
data, from_peer_id, sequence |
Relayed E2E encrypted data from another peer |
session_resumed |
missed_messages[], last_acked_sequence |
Reconnection with buffered messages |
claim_success |
server_fingerprint, message, recovery_key |
Server claimed successfully |
claim_rejected |
reason |
Claim attempt rejected |
error |
code, message |
Error response |
kicked |
reason |
Server-initiated kick |
heartbeat_pong |
timestamp, server_time |
Server heartbeat response |
| Code | Meaning |
|---|---|
400 |
Bad request — malformed frame, invalid parameters, or missing required fields |
401 |
Unauthorized — invalid signature, expired timestamp, or invalid session token |
403 |
Forbidden — server already claimed, peer not authorized, or operation not permitted |
404 |
Not found — pool or invitation does not exist |
428 |
Precondition required — server is unclaimed, send claim_server first |
429 |
Rate limited — too many requests, retry after backoff |
503 |
Service unavailable — max pools reached, pool full, or server is shutting down |
| Endpoint | Port | Description |
|---|---|---|
ws://host:9090/ |
9090 | WebSocket relay (client connections) |
GET /health |
9091 | JSON health status |
GET /metrics |
9091 | Prometheus metrics |
StealthRelay/
├── Cargo.toml # Workspace manifest (Rust 2024 edition)
├── Cargo.lock
├── Cross.toml # Cross-compilation config for CI binaries
├── Dockerfile # Multi-stage build (Debian bookworm)
├── config/
│ └── default.toml # Annotated default configuration
├── docker/
│ ├── docker-compose.yml # Base deployment
│ └── docker-compose.cloudflared.yml # Cloudflare Tunnel overlay
├── crates/
│ ├── stealthos-server/ # CLI, config, handler, claim flow
│ │ ├── src/
│ │ │ ├── main.rs # clap CLI, tokio runtime, signal handling
│ │ │ ├── app.rs # AppState shared across connections
│ │ │ ├── config.rs # TOML + env config loading
│ │ │ ├── handler.rs # WebSocket message dispatch
│ │ │ ├── claim.rs # Server claim/reclaim logic
│ │ │ └── setup.rs # Token-protected setup page with QR code
│ │ └── tests/
│ │ └── integration.rs # Integration tests
│ ├── stealthos-core/ # Pool registry, routing, types
│ │ └── src/
│ │ ├── server_frame.rs # All client/server frame types
│ │ ├── pool_registry.rs # Pool lifecycle management
│ │ ├── pool.rs # Single pool state
│ │ ├── router.rs # Message routing
│ │ ├── ratelimit.rs # Per-IP and per-connection rate limiting
│ │ ├── message.rs # Message types
│ │ ├── types.rs # PeerId, PoolId type aliases
│ │ └── error.rs # PoolError, RateLimitError
│ ├── stealthos-crypto/ # Cryptographic primitives
│ │ └── src/
│ │ ├── identity.rs # Ed25519 host identity
│ │ ├── peer_identity.rs # Peer identity verification
│ │ ├── handshake.rs # Noise NK key exchange
│ │ ├── envelope.rs # Encrypted envelope handling
│ │ ├── invitation.rs # Invitation token HKDF/HMAC
│ │ ├── pow.rs # Proof-of-work challenge/verify
│ │ └── error.rs # CryptoError types
│ ├── stealthos-transport/ # WebSocket server, TLS
│ │ └── src/
│ │ ├── server.rs # Transport server
│ │ ├── listener.rs # TCP/TLS listener
│ │ ├── connection.rs # WebSocket connection actor
│ │ ├── connection_registry.rs # Connection tracking
│ │ ├── config.rs # Transport configuration
│ │ ├── types.rs # Transport types
│ │ └── error.rs # Transport errors
│ └── stealthos-observability/ # Logging, metrics, health
│ └── src/
│ ├── logging.rs # tracing + tracing-subscriber setup
│ ├── metrics.rs # Prometheus metrics exporter
│ └── health.rs # /health and /metrics HTTP handlers
└── scripts/
├── install.sh # Linux/macOS installer (bash)
├── install.ps1 # Windows installer (PowerShell)
└── e2e-test.sh # End-to-end test runner
# Build the workspace
cargo build --workspace
# Run all tests (166 unit tests)
cargo test --workspace
# Lint
cargo clippy --workspace -- -D warnings
# Format check
cargo fmt --all -- --check
# Run locally with pretty logging
STEALTH_CRYPTO__KEY_DIR=/tmp/stealth-keys \
STEALTH_LOGGING__FORMAT=pretty \
cargo run -p stealthos-server -- serve
# E2E test against a running server
./scripts/e2e-test.sh ws://localhost:9090
# Docker build
docker build -t stealth-relay .A React web app lets friends without StealthOS or an iPhone join your pool from any browser. Same E2E encrypted protocol, no app install required.
Hosted version: web.stealthos.app — paste an invitation link and join instantly.
Self-host with Docker:
docker run -d -p 8080:80 ghcr.io/olib-ai/stealth-relay-web:latestOr build from source:
cd web-client
npm install
npm run build
# Serve web-client/dist/ with any static hosting (Nginx, Caddy, Vercel, etc.)Supports group and private chat, voice messages, image sharing, reactions, polls, and multiplayer games (Chess, Connect Four, Chain Reaction).
If you lose access to your device:
- Enter the recovery key (shown once at claim time) in the app
- The server rebinds to your new device's key
- A new recovery key is issued (the old one is invalidated)
If you lose the recovery key too, you must redeploy with a fresh key volume:
docker volume rm stealth-relay-keys
docker restart stealth-relay- Docker 24+ and Compose v2.4+ (for deployment)
- Rust 1.87+ with 2024 edition support (for building from source)
- Platforms: amd64, arm64
MIT License
Copyright (c) 2025 Olib AI
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- Olib AI — Project maintainer and StealthOS developer
- ConnectionPool — Swift P2P mesh networking client library
- PoolChat — End-to-end encrypted chat built on ConnectionPool
Contributions are welcome! Please ensure:
- All code compiles with
#![forbid(unsafe_code)] cargo clippy --workspace -- -D warningspasses with no warningscargo fmt --all -- --checkproduces no diffs- All public APIs are documented with
///doc comments - New features include unit tests
- No new dependencies without discussion in an issue first
If you discover a security vulnerability, please report it privately to security@olib.ai rather than opening a public issue.