Running your own relay
The relay carries encrypted clips between your devices. It never sees anything but a room hash and ciphertext, and it stores nothing — but you do not have to take that on trust, because you can run it yourself. It is one Python file with no database.
The published image is waiting on the first release. The
docker run line below is what the release pipeline pushes
to GHCR; until it has run against a tag, use the from-source path in
the next section, which works today.
Run it
docker run -p 8000:8000 ghcr.io/akshaynikhare/realtimeclipboard-relay
Or from a clone, with no container at all:
pip install -r backend/requirements.txt
python -m uvicorn main:app --app-dir backend --port 8000
That is the whole relay. No database, no volume, no migration step, and nothing to back up — every session lives in memory and is gone when the process exits, which is the design rather than a limitation.
Check it worked
curl http://localhost:8000/health
It answers with the instance's identifier. That field is not decoration: it is how a client can tell it has been load-balanced onto a different replica mid-session, which is the failure this design has to be careful about — see the replicas section below.
Point the app at it
- Open the app and go to Settings → Relay.
- Enter your relay's URL —
ws://localhost:8000for a local test,wss://clip.example.comfor a real one. - The status bar reads Connected once it answers. Do the same on a second device and sync a clip between them; nothing has touched the public relay.
The command-line client takes --relay or
REALTIMECLIPBOARD_RELAY for the same purpose.
TLS and a real hostname
cd deploy
REALTIMECLIPBOARD_DOMAIN=clip.example.com docker compose up -d
The deploy directory has a compose file that puts Caddy in front of the relay and obtains a certificate automatically, and a Helm chart beside it for Kubernetes.
TLS is not optional in practice. Browsers only expose
crypto.subtle in a secure context, so a page served over
plain HTTP on a LAN address cannot derive a key at all — the app cannot
work there regardless of what the relay does.
Before you scale it out
Pin replicas to one, or give it Redis. A room is held in the
memory of the process serving it, so two replicas behind a round-robin
load balancer put the two halves of a session on different machines and
neither sees the other. It presents as clips silently not arriving,
which looks like a client bug and is not one. The Helm chart refuses
replicaCount > 1 without Redis for exactly this reason.
The self-hosting notes
cover the environment variables and the HA path in full.
When it does not work
Connected, but clips never arrive
Almost always more than one replica without shared state — see above.
Compare the /health identifier from each device: if they
differ, that is the whole answer.
The app cannot reach it at all
Two candidates. The page's own Content-Security-Policy lists the relay
origins it may connect to, so a self-hosted copy of the static site
needs connect-src updated in index.html,
app.html and each page under src/pages/. And
if you are using the hosted site with your own relay, the hosted
policy will not permit it — self-host both halves.
It connects then drops, repeatedly
A reverse proxy that is not forwarding WebSocket upgrades. The client
then falls back to server-sent events plus POST and keeps working, more
slowly, which is why this shows up as sluggishness rather than
failure. Forward Upgrade and Connection.
An idle session disappears
Rooms are reaped when empty. That is deliberate — a relay that keeps nothing has nothing to leak.
Shutting it down
docker compose down # or: docker rm -f <container>
There is no data to delete. Everything the relay held was in memory, and it was ciphertext.
The full operational notes, including the environment-variable table and the high-availability path: docs/SELF-HOSTING.md. Every platform: the installation guide. Something wrong or out of date here? Open an issue.