Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/relay-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ jobs:
context: relay-server
platforms: linux/amd64
push: true
build-args: |
VERSION=${{ github.ref_name }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=rxcode-relay
Expand Down
6 changes: 6 additions & 0 deletions Packages/Sources/RxCodeSync/Transport/RelayClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ public actor RelayClient {
closeSocketLocally()
guard shouldReconnect else { return }
reconnectAttempt += 1
// Exponential backoff capped at 30s. Clamp the attempt counter once it
// reaches the ceiling: left unbounded, pow(2, n) eventually exceeds
// Int.max (then +inf) and the Int() conversion traps *before* min(30,…)
// can clamp it. 2^5 = 32 > 30, so the exponent never needs to exceed 5.
let maxBackoffExponent = 5
if reconnectAttempt > maxBackoffExponent { reconnectAttempt = maxBackoffExponent }
let delay = min(30, Int(pow(2.0, Double(reconnectAttempt))))
logger.error("[Relay] socket failed relay=\(self.relayURL.absoluteString, privacy: .public) error=\(error.localizedDescription, privacy: .public) reconnectIn=\(delay, privacy: .public)s")
updateState(.reconnecting(nextAttemptInSeconds: delay))
Expand Down
2 changes: 1 addition & 1 deletion RxCode/Services/RelayPresetCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ final class RelayPresetCatalog {
RelayPreset(
id: "rxlab-hosted",
name: "RxLab Hosted Relay",
url: "wss://relay.code.rxlab.app",
url: "wss://relaycode.rxlab.app",
description: "Official end-to-end encrypted relay operated by RxLab. Recommended for most setups.",
recommended: true
)
Expand Down
5 changes: 4 additions & 1 deletion relay-server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ WORKDIR /src
COPY go.mod go.sum* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/relay .
# VERSION is stamped into main.version and surfaced at /healthz. Defaults to
# "dev"; the relay-deploy workflow passes the git release tag on release builds.
ARG VERSION=dev
RUN CGO_ENABLED=0 go build -ldflags "-X main.version=${VERSION}" -o /out/relay .

FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/relay /relay
Expand Down
2 changes: 1 addition & 1 deletion relay-server/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func healthHandler(hub *Hub, apnsSender *PushSender, fcmSender *FCMSender) http.
"apns": apnsSender != nil,
"fcm": fcmSender != nil,
"mode": mode,
"version": "0.2.0",
"version": version,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
apiVersion: apps/v1
kind: Deployment
kind: DaemonSet
metadata:
name: rxcode-relay
namespace: rxcode-relay
labels:
app: rxcode-relay
spec:
# Safe to scale: the Redis backplane (REDIS_URL) routes envelopes across
# replicas, so a desktop and mobile peer may land on different pods.
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
# One relay pod per node. Each node lives in a different AWS region
# (ap-east-1 / ap-southeast-1 / …), so a DaemonSet pins exactly one replica
# per region and auto-extends as new regional nodes join the cluster — no
# replica count or topology labels to maintain. The Redis backplane
# (REDIS_URL) still fans WebSocket envelopes across every pod, so a desktop
# and its mobile peer may connect to relays in different regions.
selector:
matchLabels:
app: rxcode-relay
updateStrategy:
type: RollingUpdate
rollingUpdate:
# Keep serving WebSocket clients on the surviving regional pods while one
# node updates. With only one pod per node, maxUnavailable: 1 rolls a
# single region at a time.
maxUnavailable: 1
template:
metadata:
labels:
Expand Down
26 changes: 0 additions & 26 deletions relay-server/k8s/hpa.yaml

This file was deleted.

28 changes: 12 additions & 16 deletions relay-server/k8s/ingress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,20 @@ metadata:
namespace: rxcode-relay
labels:
app: rxcode-relay
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
# WebSocket connections are long-lived: the relay only pings every 25s, so
# the proxy must not time out an otherwise idle /ws stream, and responses
# must stream through unbuffered.
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
# No cert-manager / TLS block: this cluster has no cert-manager, and
# relaycode.rxlab.app is fronted by a proxied Cloudflare Load Balancer that
# terminates the public TLS cert (Cloudflare SSL mode "Full"). The
# Cloudflare→origin leg rides Traefik's default self-signed cert, so the
# Ingress itself needs no `tls:` section. DNS for the host is owned by the
# cluster-service Pulumi stack — do not create a Cloudflare record here.
#
# Traefik (the MicroK8s ingress addon) handles WebSocket `/ws` upgrades
# natively, so the old nginx websocket/proxy-timeout annotations are dropped
# — Traefik ignores `nginx.ingress.kubernetes.io/*` entirely.
spec:
ingressClassName: nginx
tls:
- hosts:
- relay.code.rxlab.app
secretName: rxcode-relay-tls
ingressClassName: public
rules:
- host: relay.code.rxlab.app
- host: relaycode.rxlab.app
http:
paths:
- path: /
Expand Down
10 changes: 7 additions & 3 deletions relay-server/k8s/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ resources:
# secrets.yaml is intentionally excluded — apply it manually after filling in
# real APNs values: `kubectl apply -f k8s/secrets.yaml`.
- configmap.yaml
- deployment.yaml
# DaemonSet (one pod per node) instead of a Deployment + HPA: nodes are
# single-per-region, so one relay per node == one relay per region, and the
# set auto-extends as regional nodes join. No HPA — DaemonSets scale with
# nodes, not CPU.
- daemonset.yaml
- service.yaml
- ingress.yaml
- pdb.yaml
- hpa.yaml

namespace: rxcode-relay

images:
- name: ghcr.io/rxtech-lab/rxcode-relay
newTag: latest
# Latest release cut from git (HEAD 6bda708). Bump on each relay release.
newTag: v1.14.0
6 changes: 6 additions & 0 deletions relay-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import (
"github.com/joho/godotenv"
)

// version is the relay's reported build version, surfaced at /healthz. It
// defaults to "dev" for local/`go run` builds and is overridden at release
// build time via -ldflags "-X main.version=<tag>" (see Dockerfile + the
// relay-deploy workflow, which passes the git release tag).
var version = "dev"

func main() {
// Load .env before reading any env vars so flag defaults see them.
// Path overridable via RELAY_ENV_FILE; default ".env" in CWD. Missing
Expand Down
4 changes: 2 additions & 2 deletions website/public/relays.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"version": 1,
"updatedAt": "2026-05-20",
"updatedAt": "2026-06-03",
"relays": [
{
"id": "rxlab-hosted",
"name": "RxLab Hosted Relay",
"url": "wss://relay.code.rxlab.app",
"url": "wss://relaycode.rxlab.app",
"description": "Official end-to-end encrypted relay operated by RxLab. Recommended for most setups.",
"recommended": true
}
Expand Down
Loading