Skip to content

RTLS relay (updates.wantok.cz/rtls) ​

The RTLS relay carries the RTLS field test between a SafeCall box and the Theseus RTLS testing tool. The box sits behind the hospital VPN and only makes outbound HTTPS calls; the tester's phone cannot reach it. Both talk to the relay instead, one channel per test QR:

Theseus (phone)  --HTTPS-->  rtls-relay (updates.wantok.cz/rtls)  <--HTTPS, outbound only--  SafeCall box

The relay is a mailbox. It keeps the latest view the box published, the actions the phone sent and the floor image and layout the box uploaded, and it never reads them. It runs on the update host (SSH alias mnew.ssh) next to the bug collector, on port 8793 behind nginx.

Do not put RTLS_RELAY_SHARED_SECRET, tokens, .env files or hospital map images in git.

Automatic deploy ​

On each release commit (bun commit → server build via scripts/build/server.sh):

  1. Right after the binary smoke test, scripts/build/rtls-relay.sh --skip-upload runs the relay tests and compiles releases/rtls-relay-x64. A failing relay test stops the release before anything ships.

  2. After the keryx-status deploy, if rtls-relay.service exists on mnew.ssh, scripts/build/rtls-relay.sh --upload-only copies the binary to mnew.ssh:/home/thomas/rtls-relay/rtls-relay.new, renames it, and restarts the service:

    bash
    sudo /bin/systemctl restart rtls-relay.service

    A failed deploy prints WARN: rtls-relay deploy failed and the release goes on. Without the unit (before OA-AT-rtls-relay) it prints WARN: rtls-relay.service not installed (OA-AT-rtls-relay), skipped.

--server-only / --skip-upload on server.sh skip both steps.

A restart drops the long polls in flight. The phone and the box reconnect by themselves and every channel survives in SQLite. Still, avoid a release while a field-test session is running.

Manual:

bash
./scripts/build/rtls-relay.sh --skip-upload   # test + compile only (bun run build:rtls-relay)
./scripts/build/rtls-relay.sh                 # test + compile + deploy (bun run deploy:rtls-relay)
./scripts/build/rtls-relay.sh --upload-only   # deploy the binary built before

One-time server setup ​

Run on mnew.ssh as a user with sudo.

1. Directory ​

bash
mkdir -p /home/thomas/rtls-relay
chown thomas:thomas /home/thomas/rtls-relay
chmod 700 /home/thomas/rtls-relay

2. Environment file ​

Create /home/thomas/rtls-relay/rtls-relay.env (never deployed from git) and make it readable by thomas only (chmod 600):

bash
RTLS_RELAY_PORT=8793
RTLS_RELAY_HOSTNAME=127.0.0.1
RTLS_RELAY_DB_PATH=/home/thomas/rtls-relay/rtls-relay.db
# Same value as BUG_COLLECTOR_SHARED_SECRET in bug-collector.env
RTLS_RELAY_SHARED_SECRET=replace-me
# Optional, default 8388608 (8 MiB): largest map image / layout blob
# RTLS_RELAY_MAX_BLOB_BYTES=8388608

RTLS_RELAY_SHARED_SECRET must equal server_bug_collector_shared_secret on the installations: a box registers its channels with that secret, so it needs no new configuration. The secret only allows registering a channel. Reading or writing a channel needs that channel's own token. The service refuses to start without the secret.

3. systemd unit ​

Create /etc/systemd/system/rtls-relay.service:

ini
[Unit]
Description=SafeCall RTLS field-test relay
After=network.target

[Service]
Type=simple
User=thomas
WorkingDirectory=/home/thomas/rtls-relay
ExecStart=/home/thomas/rtls-relay/rtls-relay
Restart=on-failure
EnvironmentFile=/home/thomas/rtls-relay/rtls-relay.env

[Install]
WantedBy=multi-user.target

Register and enable it. It starts with the first deploy (step 6), which puts the binary in place:

bash
sudo systemctl daemon-reload
sudo systemctl enable rtls-relay.service

4. sudoers (passwordless restart) ​

Add this with visudo or a drop-in under /etc/sudoers.d/:

thomas ALL=(root) NOPASSWD: /bin/systemctl restart rtls-relay.service

The path must be exactly /bin/systemctl and the unit name exact. Check with sudo -l.

5. nginx ​

Add this to the updates.wantok.cz server block, next to location /bugs/:

nginx
location /rtls/ {
    proxy_pass http://127.0.0.1:8793/;
    proxy_http_version 1.1;
    proxy_read_timeout 90s;
    proxy_buffering off;
    client_max_body_size 10m;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}
  • The trailing / in proxy_pass strips /rtls. The relay accepts paths with or without the prefix anyway.
  • proxy_read_timeout 90s: the phone and the box hold a request open for up to 25 s (long poll). The relay keeps idle connections for 60 s.
  • proxy_buffering off sends each answer the moment the relay has it.
  • client_max_body_size 10m is needed for the floor image (up to 8 MiB). The nginx default of 1 MiB would refuse it with 413.

Then run sudo nginx -t && sudo systemctl reload nginx.

6. First deploy and check ​

From the repository (or wait for the next bun commit):

bash
./scripts/build/rtls-relay.sh

It uploads the binary and restarts (that is, starts) the service. Check:

bash
sudo systemctl status rtls-relay.service              # on mnew.ssh → active (running)
curl -sS http://127.0.0.1:8793/health                 # on mnew.ssh → ok
curl -sS https://updates.wantok.cz/rtls/health        # anywhere → ok

From then on every bun commit deploys the relay by itself.

Hospital IT clearance ​

The box calls only https://updates.wantok.cz (TCP 443, outbound), the host every SafeCall install already uses for updates and the bug collector. A site that has cleared updates.wantok.cz by host name or IP needs nothing new. A site that filters by URL path must also allow /rtls/. The phone reaches the same host over mobile data or guest Wi-Fi. Nothing connects into the hospital.

Endpoints ​

Public URL = https://updates.wantok.cz/rtls + path.

MethodPathAuth
GET/healthnone
POST/v1/channelsBearer = shared secret
DELETE/v1/channels/{id}box token (rb_…)
PUT/v1/channels/{id}/statebox token
GET/v1/channels/{id}/actions?after=&wait=box token
PUT/v1/channels/{id}/blobs/{sha256}box token
GET/v1/channels/{id}/state?after=&wait=phone token (rp_…)
POST/v1/channels/{id}/actionsphone token
GET/v1/channels/{id}/blobs/{sha256}phone or box token

Limits:

LimitValue
State512 KiB
Action16 KiB
Blob8 MiB
Blobs per channel8 (least recently used evicted)
Actions per GET100
Actions kept per channel2,000
Actions per channel and minute (phone)120 (then 429 with Retry-After)
Long pollat most 25 s
Channel lifetimeat most 7 days
Live channels10 per server_id, 500 in total (then 429)

Errors are {"error": "<code>"} with 400 bad_request, 401 unauthorized, 404 not_found, 405 method_not_allowed, 409 conflict, 410 gone, 413 too_large and 429 rate_limited.

Data kept ​

  • Per channel: the SHA-256 of the box and phone tokens (never the tokens), the latest view, the queued actions and the blobs (floor image and layout). The SafeCall box is the system of record. The relay holds no session history.
  • Purge: every 10 minutes the relay deletes the view, the actions and the blobs of every channel that expired or was revoked at least 1 hour earlier. It runs SQLite with secure_delete and truncates the WAL afterwards, so the deleted data does not stay in the files. A data-free tombstone (channel id, server_id, token hashes, times) stays until 7 days after the channel's expiry. Until then, late calls get 410 and the channel cannot be registered again.
  • Logs (journalctl -u rtls-relay.service): one line per registration, revocation and purge (channel id and server_id). No tokens, no bodies.
  • Backups: do not back up rtls-relay.db. Nothing in it needs to be kept.

Troubleshooting ​

SymptomCheck
Service fails after restartjournalctl -u rtls-relay.service -n 50. RTLS_RELAY_SHARED_SECRET is required means the env file is missing or unreadable. Also check the binary has chmod +x.
Release prints WARN: rtls-relay.service not installedOA-AT-rtls-relay is not done yet (steps 1-5)
Release prints WARN: rtls-relay deploy failedsudoers line (step 4); ssh mnew.ssh sudo -l
502 Bad Gateway on /rtls/healthService down, or RTLS_RELAY_PORT / RTLS_RELAY_HOSTNAME not matching nginx
Mobile Access shows relay error 401 on registerRTLS_RELAY_SHARED_SECRET differs from the box's server_bug_collector_shared_secret
Mobile Access shows relay error 409The channel id exists with other tokens (box database restored from an old backup?). Revoke the QR and mint a new one.
Map never loads on the phone, nginx answers 413client_max_body_size 10m missing in location /rtls/
Polls end with 504 after 60 sproxy_read_timeout 90s missing
Phone: "Waiting for the SafeCall server…"The box has not registered the channel yet. Check that the box reaches https://updates.wantok.cz/rtls/health (curl on the box).
Phone: "SafeCall server not connected"The box has not polled for over 60 s: box offline, VPN or proxy change. Check Mobile Access on the box.
Phone: "This test QR is no longer valid"The channel expired or was revoked (410). Mint a new QR.