Documentation

Wire it up.

Everything you need to run Redline locally, put it on a real host, and wire a proxy into your own site. The kit is intentionally small — read it end to end in an afternoon.

Quickstart

Redline is the reference deployment in this repository. It is a static shell, an Ultraviolet service worker, and a WISP relay in a single Node process. You need Node 20 or newer.

bash
$ git clone https://github.com/RRedwire/red-wire
$ cd red-wire/services/redline
$ npm install
$ npm start

> redline listening on :8080
# open http://localhost:8080

Already have the repository? Skip the clone and run from services/redline. The server serves the shell, the UV runtime, BareMux, Epoxy and the WISP relay — there is nothing else to install.

HTTPS matters. Service workers only register on https:// or localhost. On a plain-HTTP host the shell will load but proxying will not start.

Architecture

Redline is a UBG stack: Ultraviolet, BareMux, and a Generic shell. Epoxy over WISP is the transport. Each layer has one job.

Request path

  1. The shell registers /sw.js at scope /service/.
  2. You navigate to /service/<encoded-url>; the service worker intercepts it.
  3. Ultraviolet fetches through BareClient, which talks to the BareMux transport.
  4. Epoxy opens an encrypted stream over the WISP WebSocket at /wisp/.
  5. The relay opens the TCP connection upstream and pipes bytes back.
  6. Ultraviolet rewrites the response and hands it to the browser.

Files that matter

  • server.js — static server, transport mounts, WISP upgrade handler, /health.
  • public/sw.js — imports the UV bundle, config and worker.
  • public/uv.config.js — prefix, codec and asset paths.
  • public/app.js — registers the worker, sets the transport, drives the UI.
  • public/index.html — the shell.
No upstream dependency. Unlike a reverse-proxied proxy shell, Redline serves every byte itself. The only external dependency is the destination site.

Deploy with Docker

A Dockerfile and Compose file ship with the repo. From the repository root:

bash
$ docker compose up --build -d
> redline listening on :8080

# or build the image directly
$ docker build -t redline ./services/redline
$ docker run -p 8080:8080 -e PORT=8080 redline

The container is a single Node process. Terminate TLS in front of it with Caddy, Nginx, Cloudflare, or your platform's edge — the relay itself speaks plain HTTP and upgrades to WebSocket.

Hosts

Fly.io

bash
$ cd services/redline
$ fly launch --no-deploy
$ fly deploy

Set the internal port to 8080. Fly terminates TLS and forwards WebSocket upgrades.

Railway / Render

Point the service at services/redline, set the start command to npm start and the port to 8080. Both platforms forward upgrades on managed domains.

VPS

Run the process under systemd, then put Caddy in front. Caddy gets a certificate automatically and proxies WebSocket without extra configuration:

Caddyfile
redline.example.com {
  reverse_proxy 127.0.0.1:8080
}

Cloudflare

Cloudflare Pages can host this marketing site and any static shell, but it cannot run the WISP relay — Pages has no long-lived WebSocket backend. Split the deployment:

  • red-wire.org — static site on Cloudflare Pages.
  • redline.red-wire.org — the proxy and relay on a container host.
  • Point a DNS record at the container and enable the proxy (orange cloud) for TLS.
bash — deploy the static site
$ npm run build
$ npx wrangler pages deploy dist --project-name redwire
> deployed to https://redwire.pages.dev
Custom domain. In the Pages project, add red-wire.org under Custom domains. Cloudflare creates the record automatically because the zone already lives in the same account.

Reverse proxy

If you put Nginx in front of Redline, the WISP upgrade has to survive the hop. Without these headers the shell loads and proxying fails with a silent handshake error.

nginx
location / {
  proxy_pass http://127.0.0.1:8080;
  proxy_http_version 1.1;
  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;
}

location /wisp/ {
  proxy_pass http://127.0.0.1:8080;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_read_timeout 3600s;
  proxy_send_timeout 3600s;
}

Configuration

The server reads two environment variables:

  • PORT — listen port, default 8080.
  • HOST — bind address, default 0.0.0.0.

The proxy prefix and codec live in public/uv.config.js. If you change the prefix, update the service-worker scope in public/app.js to match. The WISP endpoint is /wisp/ and is referenced once in public/app.js; if you move the relay, change the upgrade route in server.js as well.

Swapping the transport

BareMux is deliberately transport-agnostic. To point at a different relay, change the URL passed to setTransport:

javascript
const conn = new BareMuxConnection("/baremux/worker.js");

await conn.setTransport("/epoxy/index.mjs", [
  { wisp: "wss://relay.example.com/wisp/" },
]);

RedKit

RedKit is the extraction of Redline's client bootstrap: register the worker, set the transport, encode a URL, navigate. The module ships at packages/redkit/redkit.js. Copy it into your static root and mount the UV and BareMux assets alongside it.

javascript
import { createProxy } from "/redkit/redkit.js";

const wire = await createProxy({
  sw: "/sw.js",
  scope: "/service/",
  transport: "/epoxy/index.mjs",
  wisp: "wss://proxy.example.com/wisp/",
});

wire.go("https://example.com");

createProxy resolves once the worker is active and the transport is set. It rejects if the page is not served over HTTPS (except on localhost) or if the worker fails to register.

Signal

Signal is a static link hub driven by one JSON file. Point it at every mirror you run; it renders a filterable page and never phones home.

links.json
{
  "name": "Signal",
  "updated": "2026-11-06",
  "links": [
    { "name": "Infrared", "url": "https://infrared.earth", "tag": "flagship", "status": "live" },
    { "name": "Redline", "url": "https://redline.example.com", "tag": "self-hosted", "status": "live" }
  ]
}

Deploy the folder anywhere static. If a mirror dies, remove it from the manifest — there is no build step and no cache to bust.

Troubleshooting

The shell loads, but /service/ is blank

  • Open DevTools → Application → Service Workers. If /sw.js is not activated, the page is not on HTTPS or localhost.
  • Confirm the scope is /service/ and that uv.config.js uses the same prefix.
  • Hard reload once. A stale worker from a previous prefix will keep intercepting the old path.

The relay returns 502 / the transport never opens

  • Check that your reverse proxy forwards Upgrade and Connection.
  • Open the WISP URL directly with a WebSocket client; a 404 means the upgrade handler is not mounted.
  • Some platforms strip upgrades on their free tier. Test against localhost first to isolate the host.

Some sites break

This is inherent to service-worker proxying. DRM (Widevine), hardware WebAuthn, strict CSP with frame-ancestors, and WebRTC-heavy apps are common failures. There is no configuration flag that fixes them.

Updates don't apply

/sw.js is served with Cache-Control: no-store in the bundled server. If you front it with a CDN, exclude that path from caching. Bump the ?v= query string on your own assets when you change them.

Operational notes

A relay is infrastructure. Budget for bandwidth, keep the dependency lockfile pinned, and put a request limit in front of public deployments. Epoxy terminates TLS end-to-end, so the relay sees encrypted streams — but it still sees destination hosts and volumes.

Run RedWire on networks you control or have permission to use. A proxy changes how bytes travel; it doesn't grant access to content you couldn't otherwise reach.