Appearance
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 boxThe 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.
- Contract (routes, limits, auth):
lib/src/rtls_relay.ts - Source:
rtls-relay/(Bun, SQLite) - Operator log: OA-AT-rtls-relay in the Ariadne / Theseus operator actions
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):
Right after the binary smoke test,
scripts/build/rtls-relay.sh --skip-uploadruns the relay tests and compilesreleases/rtls-relay-x64. A failing relay test stops the release before anything ships.After the keryx-status deploy, if
rtls-relay.serviceexists onmnew.ssh,scripts/build/rtls-relay.sh --upload-onlycopies the binary tomnew.ssh:/home/thomas/rtls-relay/rtls-relay.new, renames it, and restarts the service:bashsudo /bin/systemctl restart rtls-relay.serviceA failed deploy prints
WARN: rtls-relay deploy failedand the release goes on. Without the unit (before OA-AT-rtls-relay) it printsWARN: 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 beforeOne-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-relay2. 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=8388608RTLS_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.targetRegister 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.service4. sudoers (passwordless restart)
Add this with visudo or a drop-in under /etc/sudoers.d/:
thomas ALL=(root) NOPASSWD: /bin/systemctl restart rtls-relay.serviceThe 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
/inproxy_passstrips/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 offsends each answer the moment the relay has it.client_max_body_size 10mis 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.shIt 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 → okFrom 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.
| Method | Path | Auth |
|---|---|---|
GET | /health | none |
POST | /v1/channels | Bearer = shared secret |
DELETE | /v1/channels/{id} | box token (rb_…) |
PUT | /v1/channels/{id}/state | box 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}/actions | phone token |
GET | /v1/channels/{id}/blobs/{sha256} | phone or box token |
Limits:
| Limit | Value |
|---|---|
| State | 512 KiB |
| Action | 16 KiB |
| Blob | 8 MiB |
| Blobs per channel | 8 (least recently used evicted) |
| Actions per GET | 100 |
| Actions kept per channel | 2,000 |
| Actions per channel and minute (phone) | 120 (then 429 with Retry-After) |
| Long poll | at most 25 s |
| Channel lifetime | at most 7 days |
| Live channels | 10 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_deleteand 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 andserver_id). No tokens, no bodies. - Backups: do not back up
rtls-relay.db. Nothing in it needs to be kept.
Troubleshooting
| Symptom | Check |
|---|---|
| Service fails after restart | journalctl -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 installed | OA-AT-rtls-relay is not done yet (steps 1-5) |
Release prints WARN: rtls-relay deploy failed | sudoers line (step 4); ssh mnew.ssh sudo -l |
502 Bad Gateway on /rtls/health | Service down, or RTLS_RELAY_PORT / RTLS_RELAY_HOSTNAME not matching nginx |
Mobile Access shows relay error 401 on register | RTLS_RELAY_SHARED_SECRET differs from the box's server_bug_collector_shared_secret |
Mobile Access shows relay error 409 | The 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 413 | client_max_body_size 10m missing in location /rtls/ |
| Polls end with 504 after 60 s | proxy_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. |