The relay is only needed for device pairing. After pairing, all auth is P2P with no relay involved.
Pairing (~30s): Device A <-WS-> Relay <-WS-> Device B Runtime: Device A --HTTP--> Device B (no relay)
The relay is stateless. Sessions exist in memory for ~30 seconds during pairing, then are forgotten. No database, no persistence.
The simplest way to self-host.
$ git clone https://github.com/ameshdev/amesh.git $ cd amesh $ docker compose up -d # Health check $ curl http://localhost:3001/health {"status":"ok","sessions":0}
# Or build the image separately $ docker build -f Dockerfile.relay -t amesh-relay . $ docker run -p 3001:3001 amesh-relay
Scales to zero — no cost when nobody is pairing. Native WebSocket support.
# Build and push gcloud builds submit \ --tag gcr.io/YOUR_PROJECT/amesh-relay \ -f Dockerfile.relay . # Deploy gcloud run deploy amesh-relay \ --image gcr.io/YOUR_PROJECT/amesh-relay \ --port 3001 \ --allow-unauthenticated \ --session-affinity \ --min-instances 0 \ --max-instances 3 \ --set-env-vars AMESH_TRUST_PROXY=1
--session-affinity keeps WebSocket connections on the same instance during pairing.
Required: set AMESH_TRUST_PROXY=1 on Cloud Run (or any reverse proxy / load balancer). The TCP peer Cloud Run exposes is the front-end LB, identical for every client — without this env var the relay's per-IP rate limiter collapses into a single global bucket. Do NOT set it on directly-exposed deployments where clients could spoof X-Forwarded-For.
Simple CLI deployment with auto-stop when idle.
$ fly launch --dockerfile Dockerfile.relay $ fly deploy
No Docker required.
$ npm install @authmesh/relay $ PORT=3001 bunx @authmesh/relay
// Or programmatically import { createRelayServer } from '@authmesh/relay'; const relay = await createRelayServer({ port: 3001 }); await relay.start();
apiVersion: apps/v1 kind: Deployment metadata: name: amesh-relay spec: replicas: 1 selector: matchLabels: app: amesh-relay template: spec: containers: - name: relay image: node:22-slim # build your own image — see Dockerfile below ports: - containerPort: 3001 readinessProbe: httpGet: path: /health port: 3001
The relay is designed to be untrusted:
AMESH_TRUST_PROXY=1 so the left-most X-Forwarded-For entry is used instead of the LB peer.PORT Listen port. Default: 3001HOST Listen address. Default: 0.0.0.0That's it. No database, no secrets, no external services.