Skip to main content
Run the open relay on your own box with no Cloudflare account. It is a single Node process: project config lives in one JSON file, screenshots live on disk, and rate limiting is in-memory. Put a TLS reverse proxy in front of it.

Quickstart (Docker)

  1. Get docker-compose.yml. There are two ways to do this. The shorter path: take docker-compose.yml on its own, with no clone. Create .env first (step 2 below); Compose reads it via env_file and errors before doing anything else if it is missing. With .env in place, docker compose up pulls and runs the published image directly. The image is ghcr.io/greenwichsteps/burnside-relay, published for both linux/amd64 and linux/arm64, so it runs on an ARM box or an Apple Silicon machine without emulation. Clone the repository instead if you want to read the source or run a modified relay:
    A published image exists either way, so plain docker compose up still pulls it rather than building your checkout. To run your own code, build explicitly with docker compose up --build (or docker compose build first). One caveat on the published image while it is still at 0.1.1: it was built before the relay started shipping its licence inside the image, so the copy you pull today does not contain the AGPL text. Building from a clone with --build does include it. The next release replaces the published tags with an image that carries it.
  2. Create a data directory and an .env file next to docker-compose.yml:
  3. Start it:
  4. Put a TLS reverse proxy (Caddy, nginx, or Cloudflare Tunnel) in front and point PUBLIC_BASE_URL at the https origin it serves. HTTPS is required for the admin panel: its session cookie is named __Host-bs_admin, and a browser takes a __Host- cookie only from an origin it treats as secure.
  5. Claim the admin panel at https://feedback.example.com/admin, using the ADMIN_SETUP_TOKEN and a password of at least 8 characters. Add a project, set its allowed origins, and connect Linear, GitHub, or GitLab.
  6. Install the widget on your site with the relay origin:

Quickstart (no Docker)

This path needs Node 20 or later. The Docker image above carries its own runtime, so the requirement only applies here. The npm package is not published yet. It goes out with the first release, so npm i -g @burnsidesteps/relay returns a 404 today. Until then, build from a clone using the second set of commands below. Both are listed because the first is what you will use once the release lands. Put the settings in a .env file next to where you run it, the same file the Docker path uses:
Then:
Not as an inline prefix on the command. ADMIN_SETUP_TOKEN=... burnside-relay writes the token into your shell history, where it stays after you have finished setting up, and it is visible in the process list to anyone else on the machine while the command runs. The token claims the admin panel and is not retired after first use, so it is worth the extra line. Run it under a process supervisor (systemd, pm2) and front it with a TLS proxy as above.

Building from a clone

All four build steps are needed, and in that order. relay/public is not in the repository (it is generated, and gitignored), so a server built without the first three starts cleanly, logs that it is listening, and then answers 404 for /, /admin and /widget.js. Nothing fails along the way, which is what makes it worth stating: every command exits 0. If you would rather not remember the order, pnpm --dir relay prepack runs exactly these four. This produces the same server the Docker image runs. The image runs the same four steps in its own build, which is why the Docker path needs none of this.

Configuration

All configuration is via environment variables.

Data and backups

DATA_DIR/config.json holds your project config, integration tokens, the admin password hash, and any Pro licence. It is written with 0600 permissions. Back up the whole DATA_DIR to preserve config and stored screenshots. Keep the directory private.

Screenshots

Only GitHub destinations store screenshots here. Linear and GitLab upload through their own APIs, so nothing of theirs lands on disk and retention does not apply to them. Screenshots are stored on disk under DATA_DIR/shots/ and served from /api/shot/.... There is nothing external to configure. If you set SHOT_SIGNING_KEY, the URLs are signed so only links the relay generated can be fetched. Screenshots are kept indefinitely unless you say otherwise. Set SHOT_RETENTION_DAYS to a positive number of days and the relay deletes anything older, sweeping once at startup and every six hours after that, logging a line whenever it removes something. This is yours to decide and your licence does not affect it: the disk is yours.

Compression

The relay compresses its own static assets: brotli when the browser offers it, gzip otherwise, for JavaScript, CSS, HTML, JSON and SVG. It matters most for widget.js, which every visitor to every page carrying the widget downloads, and which is about 3.6 times larger uncompressed. You do not need to configure anything, and you should not need to turn it on in your reverse proxy either. Neither nginx nor Caddy compresses proxied responses by default, which is why this is done here rather than left to them. If you have already enabled compression in your proxy, it will see a response that is already encoded and pass it through. Each file is compressed once and the result is kept in memory until the file changes, so the cost is paid on the first request after a deploy rather than on every request.

Health checks

GET /api/health reports whether the relay can reach its own storage. It needs no credential, so an uptime monitor can call it directly:
checked names what was probed, failing names what did not answer within three seconds. Any failure returns 503 with the same body shape, so a monitor can alert on the status code alone and read failing to know which part is unwell. Point your uptime check here rather than at the relay’s root page. That page is a static file and renders identically whether or not the relay can do anything at all. The result is cached for five seconds, so a monitor polling faster than that gets the same answer twice, and a recovery can take up to five seconds to show.

Rotating a secret

Two of the values above can be changed after the relay is running, and each costs something specific. Neither cost is recoverable afterwards, so it is worth knowing before you rotate rather than after.

ADMIN_SETUP_TOKEN

This gates claiming the admin panel. Rotating it is cheap: set a new value and restart the relay. Nothing already working stops working, because the token is only read when the panel is claimed. The more useful action is usually to remove it. Once the panel has been claimed, an ADMIN_SETUP_TOKEN that is still set is a credential with nothing left to protect, and it becomes a live one again the moment the stored credentials are deleted. If you no longer need to claim the panel, unset it.

SHOT_SIGNING_KEY

This signs screenshot URLs. Rotating it invalidates every screenshot link already filed in your tracker: the signatures in those URLs were made with the old key, so they stop verifying and the images stop loading. The issues keep their text, and the image is still in your storage, but the links in them are dead and there is no way to re-sign an issue body that has already been written. So rotate this one only when you have a reason to believe the key itself is exposed. If you are rotating it as routine hygiene, you are trading every historical screenshot for very little. The same applies to changing its encoding: hex and standard base64 are both accepted, and the same 32 bytes written either way produce the same key, but a different value is a different key and retires the old links exactly as a rotation would.

If you think a secret has leaked

Rotate first and investigate afterwards. The order matters more than the tidiness.
  1. ADMIN_SETUP_TOKEN: unset it if the panel is already claimed. That closes the route with no side effects at all.
  2. SHOT_SIGNING_KEY: rotate, accepting the cost above. A leaked signing key means anyone holding it can mint URLs for any screenshot in your storage, which is worse than losing the old links.
  3. The admin password: change it from inside the panel. Note that a stolen session cookie currently survives a password change until it expires, so if you believe a session was taken rather than the password, treat the relay as compromised until the session TTL has passed.
  4. A destination token (the GitHub, GitLab or Linear token you configured): revoke it at the provider, not here. Removing it from the relay stops the relay using it and does nothing about anyone else who has it.