ATProto relay deployments #
experimental — this is a personal project for learning ATProto infrastructure. the endpoints below may go down, lose data, or change without notice. do not depend on them for anything that matters.
two full-network ATProto relays on independent Hetzner Cloud nodes with k3s:
| indigo (Go) | zlay (Zig) | |
|---|---|---|
| firehose | wss://relay.waow.tech |
wss://zlay.waow.tech |
| jetstream | wss://jetstream.waow.tech/subscribe (sidecar) |
— |
| collectiondir | lightrail sidecar on same endpoint | built-in (inspired by lightrail) |
| health | relay.waow.tech/xrpc/_health |
zlay.waow.tech/_health |
| metrics | relay-metrics.waow.tech |
zlay-metrics.waow.tech |
| source | bluesky-social/indigo | zat.dev/zlay |
A separate Jetstream V2 implementation is
available at wss://stream.waow.tech/subscribe (Jetstream-compatible JSON) and
wss://stream.waow.tech/subscribe-v2 (sequence cursors, CBOR records, and sync
events).
try it #
both scripts are self-contained uv scripts — no virtualenv or install needed.
firehose #
consumes the raw CBOR firehose using the atproto python SDK.
# watch posts scroll by for 10 seconds
./scripts/firehose
# run longer, filter by collection
./scripts/firehose --duration 30
./scripts/firehose --collection app.bsky.feed.like
./scripts/firehose --duration 0 # forever (ctrl-c to stop)
# point at a different relay
./scripts/firehose --relay-url wss://zlay.waow.tech
./scripts/firehose --relay-url wss://bsky.network
jetstream #
consumes the simplified JSON firehose via jetstream — no atproto SDK needed, just plain websockets.
# watch all events for 10 seconds
./scripts/jetstream
# filter to specific collections
./scripts/jetstream --collection app.bsky.feed.post
./scripts/jetstream --collection app.bsky.feed.like --collection app.bsky.graph.follow
# run longer, or forever
./scripts/jetstream --duration 30
./scripts/jetstream --duration 0 # forever (ctrl-c to stop)
# point at a different jetstream instance
./scripts/jetstream --url wss://jetstream1.us-east.bsky.network
relay-eval #
relay-eval.waow.tech — compares what ATProto relays and derived Jetstreams see by subscribing to their synchronization streams simultaneously and measuring DID coverage overlap. Jetstreams are scored against the relay-derived universe without becoming extra consensus votes. inspired by pulsar by mackuba (zlib license).
source: relay-eval/
Bluesky v1 (numbered hosts) and v2 (unnumbered US East/West hosts) are
measured independently through their /subscribe JSON feeds. For v2 this is
the compatibility interface; these measurements do not test the native
network.bsky.jetstream.subscribeEvents API. Coverage remains relative to
the relay consensus, and history begins when each host is added.
relay sonification #
relay-eval/sonify_publisher.py continuously renders the same events/sec → Hz
mapping as /sonify into synchronized five-minute sine, triangle, and square
MP3 segments. Each waveform is published as its own stable unlisted Plyr track,
whose audio is replaced every five minutes so Plyr's bounded track-revision
history acts as a rolling window. Each replacement's title and description
identify its exact UTC recording interval. All three covers are rendered from
the same one-second per-relay rate samples as the audio, using relay-eval's dark
background and colored trend traces.
Add a PLYR_TOKEN for the publishing account to the repository-root .env,
then deploy the separate service:
just relay-eval deploy-sonify
just relay-eval logs-sonify
The first fresh deployment is a 60-second bootstrap segment; subsequent
replacements are five minutes. Each stable track ID is persisted on the server
after its first successful upload. PLYR_TRACK_ID adopts an existing sine
track; PLYR_TRIANGLE_TRACK_ID and PLYR_SQUARE_TRACK_ID can adopt sibling
tracks.
Useful optional settings are RELAY_SONIFY_SEGMENT_SECONDS (default 300),
RELAY_SONIFY_INITIAL_SEGMENT_SECONDS (default 60),
RELAY_SONIFY_BITRATE_KBPS (default 96), and
RELAY_SONIFY_MASTER_GAIN (default 0.1).
At 96 kbps, each five-minute segment is about 3.6 MB per waveform. An eight-hour window is 96 segments per track (the current version plus 95 revisions), about 1.04 GB across all three waveforms. Continuous encoded volume is about 3.11 GB/day. Each waveform publisher persists cumulative segment, audio-second, and encoded-byte counters in its state file so future hourly or user-funded archive policies can account from actual output. Interval cover images are transient publisher artifacts: Plyr receives the current image, then the local PNG and sample sidecar are removed. Interrupted recordings are discarded on restart because they do not have a complete matching trace sidecar; only intervals whose audio and cover describe the same sample window are published.
The independent live sonification is a sine-wave AAC HLS stream:
https://relay-eval.waow.tech/sonify/live/index.m3u8
It uses four-second media segments and a short sliding playlist. The stable
JSON status endpoint is
https://relay-eval.waow.tech/api/sonify/live; its live value advances only
when a completed media segment is observed. The playlist is edge-cacheable for
two seconds—shorter than one media segment—while uniquely named media segments
are immutable and cross-origin readable. The status response remains uncached
and also advertises the stable square artwork at
https://relay-eval.waow.tech/sonify/live/cover.png. It shows the latest five
minutes of one-second relay-rate traces, refreshes every 30 seconds, and is
edge-cacheable for 15 seconds. This service does not use Plyr credentials and
does not replace or restart the three-track archival publisher.
just relay-eval deploy-sonify-stream
just relay-eval logs-sonify-stream
Optional settings are RELAY_SONIFY_HLS_SEGMENT_SECONDS (default 4),
RELAY_SONIFY_HLS_PLAYLIST_SEGMENTS (default 6), and
RELAY_SONIFY_STALE_RATES_SECONDS (default 5). Artwork tuning is available
through RELAY_SONIFY_COVER_WINDOW_SECONDS (default 300) and
RELAY_SONIFY_COVER_REFRESH_SECONDS (default 30).
what's here #
.
├── indigo/ # Go relay (indigo) — justfile, deploy configs, terraform
├── zlay/ # zig relay (zlay) — justfile, deploy configs, terraform
├── relay-eval/ # firehose comparison tool — zig, hetzner, systemd
├── stream/ # retired canary receipt + next dedicated Stream deployment
├── shared/deploy/ # helm values shared by both deployments
├── scripts/ # uv scripts — firehose, jetstream, backfill
├── docs/ # architecture, deployment guide, backfill
└── justfile # root — `just indigo <recipe>` / `just zlay <recipe>`
each relay is a just module with symmetric recipes:
just indigo deploy # deploy Go relay
just indigo status # check Go relay pods
just zlay deploy # deploy zig relay
just zlay status # check zig relay pods
STREAM_DEDICATED_IP=... just --justfile stream/dedicated/justfile --list
just --list # see all available recipes
why #
the ATProto relay is the piece of infrastructure that aggregates writes from every PDS on the network into a single firehose stream. downstream services (appviews, feed generators, labelers) subscribe to a relay instead of crawling thousands of individual servers.
running one is surprisingly cheap — the relay binary uses modest CPU and memory, and storage requirements are manageable. the main cost driver is bandwidth, which is why Hetzner (unlimited 1 Gbps) is a good fit.
this repo is a template for deploying your own. everything is declarative: terraform for the VM, helm for the workloads, a justfile to tie it together. see docs/deploying.md for setup instructions and docs/architecture.md for how the pieces fit together.
prior art #
- a full-network relay for $34 a month by bryan newbold — the definitive guide
- atproto relay any% speedrun — proof it runs on a raspberry pi
- running a PDS in kubernetes — the app-template helm pattern
- firehose.network — 3 public relays deployed globally