Spooky is an HTTP/3 (QUIC) edge reverse proxy. Clients connect over HTTP/3; Spooky forwards to your existing HTTP/2 backends unchanged. This guide gets a working proxy running locally and confirms the full upgrade path — including the Alt-Svc header that tells browsers to switch to HTTP/3.

Total time: about 5 minutes.

Prerequisites

  • Rust 1.85+ (edition 2024) — rustup update stable
  • curl with HTTP/3 support — the curl that ships with macOS does not include HTTP/3. Install one that does: bash brew install curl # then use $(brew --prefix curl)/bin/curl in the commands below, or put it first on PATH Alternatively, use Spooky's own h2_backend test client (shown in Step 3) to confirm connectivity without curl.
  • UDP port 9889 free — QUIC runs over UDP. Check with lsof -iUDP:9889.

Step 1: Build

git clone https://github.com/Supernova-Labs-Org/spooky.git
cd spooky
cargo build --release

The binary lands at target/release/spooky.

Step 2: Generate a Certificate

QUIC requires TLS 1.3. For local testing, a self-signed certificate works fine:

mkdir -p certs
openssl req -x509 -newkey rsa:4096 -nodes \
  -keyout certs/key.pem \
  -out certs/cert.pem \
  -days 365 \
  -subj "/CN=localhost"

Production: see TLS Setup.

Step 3: Start a Test Backend

Spooky requires HTTP/2 backends. HTTP/1.1-only upstreams are not supported.

Use the bundled test backend, which speaks HTTP/2 out of the box:

cargo run --bin h2_backend -- --port 8080

Leave this running in its own terminal.

Step 4: Write the Config

Create config.yaml in the repository root:

version: 1                        # config schema version — must be 1

listen:
  protocol: http3                 # accept QUIC/HTTP/3 on this socket
  port: 9889                      # UDP port clients connect to
  address: "0.0.0.0"             # bind all interfaces; use 127.0.0.1 for loopback-only
  tls:
    cert: "certs/cert.pem"        # path to PEM-encoded certificate chain
    key: "certs/key.pem"          # path to PEM-encoded private key

upstream:
  default:                        # pool name — referenced internally; "default" catches all unmatched routes
    load_balancing:
      type: round-robin           # distribute requests evenly across backends in order
    route:
      path_prefix: "/"            # match every request path
    backends:
      - id: backend-1             # arbitrary label shown in logs
        address: "127.0.0.1:8080" # where to forward — must be an HTTP/2 endpoint
        weight: 100               # relative share of traffic (only meaningful with multiple backends)

log:
  level: info                     # debug | info | warn | error

Step 5: Start Spooky

./target/release/spooky --config config.yaml

You should see:

INFO spooky: loading config path="config.yaml"
INFO spooky: listening on 0.0.0.0:9889 protocol=http3
INFO spooky: upstream pool ready pool=default backends=1

Step 6: Verify HTTP/3

6a. Force HTTP/3 (confirms QUIC is working)

curl --http3-only -k https://localhost:9889/

--http3-only refuses to fall back to TCP. If this succeeds, QUIC is live.

6b. Verify the Alt-Svc upgrade path (mimics browser behavior)

Browsers don't start with HTTP/3 — they discover it via the Alt-Svc response header on a regular HTTPS request, then switch on the next connection. Test that Spooky sends this header correctly:

curl -k -I https://localhost:9889/

Look for this line in the response headers:

alt-svc: h3=":9889"; ma=86400

h3=":9889" tells the client that HTTP/3 is available on port 9889. ma=86400 is the max-age in seconds (24 hours) — how long the client should remember and prefer HTTP/3 for this origin.

If you see this header, Spooky is correctly advertising HTTP/3 to clients that don't yet support it or haven't upgraded yet.

Common Issues

Error: Address already in use — something else is bound to UDP 9889. Find it with lsof -iUDP:9889 and stop it, or change port in config.yaml.

Failed to connect to backend — the h2_backend process isn't running, or is on a different port. Confirm it's up with curl -k --http2 https://localhost:8080/ (expect a response, not a connection refused).

Failed to load TLS certificate — the paths in config.yaml don't match where you generated the files. Both certs/cert.pem and certs/key.pem must exist relative to the working directory you launch Spooky from.

curl falls back to HTTP/2 silently — you're using the system curl, which lacks HTTP/3 support. Use brew install curl and invoke it with the full path, or check curl --version for HTTP/3 in the features list.

  • Configuration Reference — every config field, its type, default value, and valid range.
  • Load Balancing Guide — when to use round-robin vs. least-connections vs. random, and how weights interact.
  • TLS Setup — production certificates with Let's Encrypt, cert rotation, and mTLS.
  • Production Deployment — systemd unit file, resource limits, metrics endpoints, and hardening checklist.