- Node.js >= 20
- A server with a public IP (VPS, dedicated, or cloud instance)
- UDP port access (HyperDHT uses UDP for peer discovery)
- At least 1 GB RAM, 10 GB disk (more disk = more apps you can seed)
From npm (stable latest is 0.24.4; the release-candidate lane publishes to
the next dist-tag):
npm install -g p2p-hiverelay # stable
npm install -g p2p-hiverelay@next # release-candidate laneOr from the signed source tag (see the stable guide for the current tag):
git clone https://github.com/bigdestiny2/P2P-Hiverelay.git /opt/hiverelay
cd /opt/hiverelay
npm ci --omit=devsudo useradd -r -s /usr/sbin/nologin -d /var/lib/hiverelay hiverelay
sudo mkdir -p /var/lib/hiverelay
sudo chown hiverelay:hiverelay /var/lib/hiverelay# Create config (optional — defaults work fine)
sudo -u hiverelay mkdir -p /var/lib/hiverelay
cat <<EOF | sudo tee /home/hiverelay/.hiverelay/config.json
{
"storage": "/var/lib/hiverelay",
"maxStorageBytes": 53687091200,
"regions": ["NA"],
"apiPort": 9100,
"enableRelay": true,
"enableSeeding": true,
"enableMetrics": true,
"enableAPI": true
}
EOFSignedDirectory (hiverelay-signed-directory) is an always-on,
openly-writable registry of signed records keyed by author pubkey.
First consumer: marketplace offer discovery (sellers publish a signed
Offer; buyers list + verify). Generic enough for any "signed record set
by author" use case (job boards, file-share directories, signed gossip
feeds).
Trust model — identical to a topic-swarm announce: the relay can
omit, reorder, or refuse records; it cannot forge them. All records are
signed over SHA256(authorPubkey || timestamp_LE || payload) using
Ed25519. Buyers MUST verify signatures + timestamps client-side; the
relay never inspects payload semantics, by design.
OFF by default. Enable per node by adding to config.json:
{
"signedDirectory": {
"enabled": true,
"maxEntryBytes": 8192,
"ttlSeconds": 86400,
"maxEntriesPerAuthor": 1,
"publishRatePerMinute": 5,
"maxTotalEntries": 10000,
"clockSkewToleranceSeconds": 60
}
}The defaults are the storage-policy commitments documented in issue #33:
maxEntryBytes(8 KB) — per-record capttlSeconds(24h) — TTL eviction; tradeoff between always-on discovery and storage churnmaxEntriesPerAuthor(1) — newest-timestamp-wins; one live record per authorPubkeypublishRatePerMinute(5, per-peer) — symmetric with circuit-relay's reserve rate limitmaxTotalEntries(10000) — bounds the in-memory store for storage-constrained operatorsclockSkewToleranceSeconds(60) — critical: rejects entries timestamped more than 60s in the future. Without this, a malicious author withtimestamp = 2099-01-01permanently outranks legitimate updates from the same pubkey. Bidirectional bound.
Replication is automatic between enabled relays. A publish to relay
A is broadcast via NOTIFY to all peer-relay channels A has open; peer
relays validate and store (no re-broadcast — single-hop). No
configuration needed beyond signedDirectory.enabled on each
participating relay.
Storage is in-memory only in v1. Short TTL (24h default) + cross- relay replication means individual-relay restarts lose state but the network as a whole stays warm. Hyperbee-backed persistence is tracked as a follow-up.
The relay-local half of the bootstrap subsidy from
docs/OPERATOR-INCENTIVES-Y1.md (Prong 2). When enabled, the node
accrues a capped sats estimate for blind-peer work (default
500 sats/day ≈ $180/yr at $100k/BTC) and records per-epoch evidence
(uptime, connections, seeded/anchored counts). No money moves through
the relay — there is no wallet in the app. The operator sets a payout
destination they control (lightning address, BOLT12 offer, or on-chain
address) from the dashboard, and the subsidy coordinator independently
verifies the relay's Ed25519-signed claim (GET /api/subsidy/claim)
before paying. The relay's own numbers are an estimate; the
coordinator's verification (Operator Score gates, sybil checks, held
schedule) decides the actual payout.
OFF by default. Enable per node via config.json:
{
"subsidy": {
"enabled": true,
"rateSatsPerDay": 500,
"epochMs": 600000,
"payoutDestination": null
}
}or via env for containerized installs: HIVERELAY_SUBSIDY_ENABLED=1
(optionally HIVERELAY_SUBSIDY_DESTINATION=you@your-ln-provider.com).
State persists to <storage>/subsidy.json with atomic writes. Surfaces:
dashboard earnings cards, /api/subsidy (status), /status (subsidy
block).
/status exposes a signedDirectory block when enabled:
{
"signedDirectory": {
"enabled": true,
"entries": 47,
"maxTotalEntries": 10000,
"attachedChannels": 11,
"ttlSeconds": 86400,
"totalPublished": 312,
"totalRejected": 4,
"totalReplicated": 89,
"totalEvicted": 0,
"rejectedReasons": { "BAD_SIGNATURE": 2, "RATE_LIMITED": 2 }
}
}Verify with npx brittle test/unit/signed-directory.test.js.
ForwardRelay (hiverelay-forward) lets this node relay app connections for
peers behind NAT / UDP-blocking, and is the building block for onion routing
(see docs/forward-relay.md). It is OFF by default — enable it per node by
adding to config.json:
{
"forwardRelay": {
"enabled": true,
"maxForwardsPerPeer": 5,
"maxForwardBytes": 67108864
}
}It only ever reaches DHT pubkeys (never an arbitrary IP — not an internet
open proxy), is bounded by per-peer concurrency + per-forward (64 MB) +
per-frame (64 KB) caps, and honours the node's SwarmFirewall. Enable it
deliberately: it lets peers reach other DHT peers through your node. Verify
with npx brittle test/integration/forward-relay.test.js.
The storage-proof builtin service lets a user trustlessly verify your
relay actually holds their app — not just claims to in the catalog. On
request it produces a signed Hypercore Merkle proof for a randomly-sampled
block of a seeded drive's metadata core; the client verifies each proof
against the drive key alone (forged content is rejected) plus your relay's
swarm-identity signature (attribution + a fresh nonce, so proofs can't be
replayed). It backs client.proveSeeded(driveKey, { relay, samples }).
It is OFF by default. Enable it per node:
-
Node runtime — add
storage-prooftoconfig.plugins(or the Services tab /services.json):{ "plugins": ["storage-proof"] } -
Bare / appliance runtime — add
storage-prooftoconfig.services, or set the env var:HIVERELAY_STORAGE_PROOF=1
Once enabled it is reachable over the existing service RPC by anyone
(route policy storage-proof.prove = public) — that is intentional:
trustless verification is only useful if any user can run it. The safety
properties that make this safe to expose:
- Privacy gate — blind / privacy-redacted drives return
NOT_SEEDED, the same error as a key the relay doesn't hold. A signed proof is cryptographic, relay-attributable evidence of possession, so serving one for a blind drive would defeat the catalog's deliberate redaction. The gate reuses the catalog's ownAppRegistry._shouldRedactEntrypredicate, soprove()can never become a possession oracle. - Rate limits — a sybil-resistant global proof-work token bucket caps total proof work regardless of how many ephemeral identities connect, with a bounded per-caller bucket on top. Tokens are spent only for real proof work; cheap rejects (bad input / not-seeded / blind) never spend them, so a not-seeded flood can't starve honest callers.
- Never reads attacker keys — the service only proves keys present in
appRegistryand never callsstore.get()on caller-supplied input, so it can't be coerced into allocating unbounded phantom Hypercores.
v1 proves the drive metadata core; blobs-core proofs are a follow-up.
Verify with npx brittle test/unit/storage-proof-service.test.js.
sudo cp /opt/hiverelay/hiverelay.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable hiverelay
sudo systemctl start hiverelaysudo systemctl status hiverelay
journalctl -u hiverelay -f # Follow logs
curl http://127.0.0.1:9100/health # Health check
curl http://127.0.0.1:9100/status # Full statusIf you're building your own image from the repo Dockerfile, or rolling
your own from scratch, use a glibc-based base (node:22-bookworm-slim,
node:20-bookworm-slim, Ubuntu, RHEL-family, etc.). Alpine/musl is not
currently supported because:
udx-nativeships prebuilds forlinux-x64/linux-arm64(glibc) but not forlinux-x64-musl/linux-arm64-musl. On Alpine,require-addondetects musl via/etc/alpine-releaseand looks for a musl prebuild that doesn't exist → first import crashes withCannot find module '/prebuilds/linux-x64-musl/udx-native.node'.sodium-nativehas the same gap.- Building from source on Alpine works but requires
cmake-bare+cmake-napi+python3+make+g++in both build and runtime stages and roughly doubles image size.
Trade-off: bookworm-slim is ~50 MB larger than Alpine but loads the
native prebuilds directly, so the runtime image stays clean and starts
without surprises.
Tracked in #21.
Upstream context: holepunchto/udx-native
— add musl prebuilds for parity with the P2P/relay/CLI ecosystem's
common Alpine deployments.
docker run -d \
--name hiverelay \
--restart unless-stopped \
-v hiverelay-data:/data \
-p 9100:9100 \
ghcr.io/bigdestiny2/p2p-hiverelay:latest \
start --storage /data --region NAcd /opt/hiverelay
docker compose up -d
docker compose logs -fdocker run -d \
--name hiverelay \
--restart unless-stopped \
--network host \
-v hiverelay-data:/data \
ghcr.io/bigdestiny2/p2p-hiverelay:latest \
start --storage /dataHost networking gives HyperDHT direct UDP access without NAT translation, which improves peer discovery and hole-punching reliability.
HiveRelay uses structured JSON logging via pino.
Set via environment variable:
HIVERELAY_LOG_LEVEL=info # Default: info
HIVERELAY_LOG_LEVEL=debug # Verbose (development)
HIVERELAY_LOG_LEVEL=warn # Quiet (production)# systemd
journalctl -u hiverelay -f
journalctl -u hiverelay --since "1 hour ago"
# Docker
docker logs -f hiverelay
# Pretty-print (install pino-pretty globally)
journalctl -u hiverelay -o cat | npx pino-prettyjournald handles rotation automatically. To configure retention:
sudo vi /etc/systemd/journald.conf
# SystemMaxUse=500M
# MaxRetentionSec=30day
sudo systemctl restart systemd-journaldScrape http://localhost:9100/metrics from your Prometheus instance:
# prometheus.yml
scrape_configs:
- job_name: hiverelay
static_configs:
- targets: ['your-server:9100']
scrape_interval: 30sAvailable metrics:
hiverelay_uptime_seconds— Node uptimehiverelay_seeded_apps— Number of apps being seeded (appRegistry size)hiverelay_bytes_stored— Total bytes storedhiverelay_bytes_served— Total bytes served to peershiverelay_cores_seeded— Cores routed throughSeeder.seedCore()(registry log core + any standalone cores). Does NOT include appRegistry-managed Hyperdrive cores — see the registry counters below.hiverelay_app_registry_entries— Total appRegistry entrieshiverelay_app_registry_anchored— Entries with blob blocks fully replicated locallyhiverelay_app_registry_unanchored— Entries waiting on the repair passhiverelay_app_registry_cores— Underlying Hypercores managed via appRegistry (≈ 2× entries: meta + blob per Hyperdrive)hiverelay_connections— Current active connectionshiverelay_active_circuits— Active relay circuitshiverelay_process_heap_bytes— V8 heap usagehiverelay_process_rss_bytes— Resident set size
Why both cores_seeded and app_registry_cores exist: the coresSeeded counter pre-dates the appRegistry-managed download-range work (PR #24 / v0.8.21). It only counts what's routed through Seeder.seedCore, which is the seedingRegistry's log core and a small number of standalone seeds. Dashboards that read it for "how much is this relay serving?" undercount by 1000× on a busy relay; switch to hiverelay_app_registry_cores for that question, and keep hiverelay_cores_seeded for "is the registry log seeding healthy?"
# Simple health check (for load balancers, uptime monitors)
curl -f http://127.0.0.1:9100/health
# Full status
curl http://127.0.0.1:9100/status
# Peer list
curl http://127.0.0.1:9100/peers/status includes a disk field with the relay's storage volume state:
{
"disk": {
"usedPct": 47,
"usedBytes": 24052555776,
"freeBytes": 26789236736,
"totalBytes": 51190108160,
"mountPath": "/data",
"status": "ok",
"checkedAt": 1779543201000
}
}status is one of:
ok— underdiskWarnThreshold(default 85%)warn— at or above warn thresholdcritical— at or abovediskCriticalThreshold(default 95%)
The check runs every 30 seconds in the background (df -kP); no I/O on
the status hot path. Override thresholds via config:
{
"diskWarnThreshold": 80,
"diskCriticalThreshold": 92,
"diskRefreshIntervalMs": 60000
}Set "diskHealthGate": true to make /health return 503 when disk
is critical, so load balancers / uptime monitors can drain traffic
before the volume actually fills. Default is false (preserves existing
/health semantics — always 200 unless the process is down).
This signal exists because #27 traced a 15-hour debugging fire to a 1 GB Fly.io volume hitting 100%. No relay-side counter surfaced the capacity pressure; the visible symptoms all looked like code bugs. This is the cheap signal that closes the observability gap.
The HTTP API binds to 0.0.0.0 by default for remote access compatibility (relays are public infrastructure). To restrict to localhost, set apiHost: '127.0.0.1' in config.
Caddy provides automatic HTTPS with Let's Encrypt certificates:
# Install Caddy
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy
# Configure
sudo tee /etc/caddy/Caddyfile <<'EOF'
relay-us.p2phiverelay.xyz {
reverse_proxy 127.0.0.1:9100
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
}
}
EOF
sudo systemctl enable caddy
sudo systemctl restart caddyCaddy auto-obtains and renews TLS certificates. No manual cert management needed.
server {
listen 443 ssl http2;
server_name relay.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
ssl_protocols TLSv1.3;
location / {
proxy_pass http://127.0.0.1:9100;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}For private relay operators who want to restrict who can seed:
# Generate and set API key
export HIVERELAY_API_KEY=$(openssl rand -hex 32)
# Add to systemd: Environment=HIVERELAY_API_KEY=<key>When HIVERELAY_API_KEY is set:
- All write endpoints (
POST /seed,/unseed,/registry/*) requireAuthorization: Bearer <key>header - Read endpoints remain open (health, metrics, catalog, gateway)
- Ownership signatures and registration challenges become available (opt-in, verified when provided)
Note: Public relays (like the official HiveRelay network) do NOT use API keys. Relays are open infrastructure — anyone can seed. Rate limiting and storage limits prevent abuse.
Built-in rate limiting protects against abuse:
- HTTP API: 60 requests/minute per IP, 64KB max request body
- P2P Protocol: Token bucket rate limiter per peer key
- Directory listings: Max 1000 entries with timeout protection
HiveRelay needs outbound UDP for HyperDHT. No specific inbound ports need to be opened — HyperDHT handles NAT traversal automatically.
# If you want to explicitly allow (optional, usually not needed):
# UDP — HyperDHT (ephemeral ports)
sudo ufw allow out proto udp
# API — only if exposing via reverse proxy
sudo ufw allow 9100/tcpDefault max storage: 50 GB. Configure with --max-storage:
hiverelay start --max-storage 100GBMonitor disk usage:
curl http://127.0.0.1:9100/status | jq '.seeder.totalBytesStored'
du -sh /var/lib/hiverelayThe relay node tracks storage per-core and will reject new seed requests when approaching the limit.
Increase --max-connections (default 256) and --max-storage. A single node can handle hundreds of concurrent peers.
Run multiple relay nodes on different machines. They discover each other automatically via the DHT — no coordination needed. Each node independently:
- Announces on the discovery topic
- Accepts seed requests based on local capacity
- Responds to proof-of-relay challenges
| Workload | RAM | Disk | CPU | Connections |
|---|---|---|---|---|
| Light (< 10 apps) | 512 MB | 10 GB | 1 core | 64 |
| Medium (10-50 apps) | 1 GB | 50 GB | 2 cores | 256 |
| Heavy (50+ apps) | 2 GB | 200 GB | 4 cores | 512 |
# Bare metal
cd /opt/hiverelay
git pull
npm ci --omit=dev
sudo systemctl restart hiverelay
# Docker
docker compose pull
docker compose up -d# Check logs
journalctl -u hiverelay --no-pager -n 50
# Check port conflict
ss -tlnp | grep 9100
# Check storage permissions
ls -la /var/lib/hiverelay# Check DHT connectivity
curl http://127.0.0.1:9100/peers
# Check if UDP is blocked
# HyperDHT needs outbound UDP access# Check metrics
curl http://127.0.0.1:9100/metrics | grep process
# Reduce connections
hiverelay start --max-connections 128
# systemd will enforce MemoryMax=2G and restart if exceededIf you seed apps but they don't show in /catalog.json:
-
Check manifest.json exists — Apps MUST have a
/manifest.jsonfile in their root:{ "id": "my-app", "name": "My App", "description": "App description", "version": "1.0.0", "categories": ["utility"] } -
Convert Pear keys correctly — Pear uses z-base-32 encoding, but the API expects hex:
// Convert pear://KEY to hex const z32 = require('z32') const hexKey = z32.decode('om5cpdjjp4g4wa15r9wjhjiex9jjmcsacwsw44hzsrtsz171ykfy').toString('hex') // Result: 82f6c68d296e8daa625b27e89e26a87fd295b2d8652d4d6b97b1236bcbb2028a
-
Check gateway can access drive — The relay must be able to replicate the drive:
# Test gateway access curl http://localhost:9100/v1/hyper/HEX_KEY/manifest.json
Problem: Caddy fails to obtain certificates or shows certificate errors.
Check domain configuration:
# Verify DNS resolves to this server
dig +short relay-us.p2phiverelay.xyz
# Check Caddy is using correct domain (NOT p2p-hiverelay.xyz with hyphen)
cat /etc/caddy/Caddyfile | grep -E "^\w+\.p2p"Common fixes:
- Ensure domain in Caddyfile matches DNS (use
p2phiverelay.xyznotp2p-hiverelay.xyz) - Clear Caddy's certificate cache if switching from staging:
rm -rf ~/.local/share/caddy/certificates - Check rate limits: Let's Encrypt allows 50 certificates per domain per week
When deploying Pear apps to HiveRelay:
- App has
manifest.jsonwith id, name, description, version - App is staged with
pear stage dev . - Pear key is converted from z-base-32 to hex format
- App is seeded via
POST /seed {"appKey": "hex", "appId": "..."} - Verify with
GET /catalog.json— app should appear within 5 seconds - Test app loads via
GET /v1/hyper/HEX_KEY/index.html