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.
$ 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:// 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
- The shell registers
/sw.jsat scope/service/. - You navigate to
/service/<encoded-url>; the service worker intercepts it. - Ultraviolet fetches through
BareClient, which talks to the BareMux transport. - Epoxy opens an encrypted stream over the WISP WebSocket at
/wisp/. - The relay opens the TCP connection upstream and pipes bytes back.
- 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.
Deploy with Docker
A Dockerfile and Compose file ship with the repo. From the repository root:
$ 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
$ 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:
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.
$ npm run build $ npx wrangler pages deploy dist --project-name redwire > deployed to https://redwire.pages.dev
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.
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, default8080.HOST— bind address, default0.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:
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.
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.
{
"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.jsis not activated, the page is not on HTTPS or localhost. - Confirm the scope is
/service/and thatuv.config.jsuses 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
UpgradeandConnection. - 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
localhostfirst 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.