fix: default RELAY_URL to the hosted relay (v0.8.2)

The default http://localhost:8080 silently failed for clinics that joined
the network without setting RELAY_URL — inside Docker localhost is the
container itself, so the hub connection never reached the relay (endless
"relay unreachable" retries) and pairing QRs encoded an unreachable
localhost. Default to https://network.temetro.com so "Join Temetro Network"
works out of the box; self-hosters running their own relay still override
it. Also (re)ensure the hub is connected before pre-registering a pairing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-07-05 22:50:46 +03:00
parent 233ce9f854
commit 01dbc07e92
8 changed files with 34 additions and 8 deletions
+14 -1
View File
@@ -7,7 +7,20 @@ for how releases are cut and published.
## [Unreleased] ## [Unreleased]
## [0.8.1] — 2026-07-05 ## [0.8.2] — 2026-07-05
### Fixed
- **`RELAY_URL` now defaults to the hosted relay** (`https://network.temetro.com`) instead of
`http://localhost:8080`. The old default silently failed for anyone who joined the network without
explicitly setting `RELAY_URL` — the backend's hub connection could never reach the relay (inside
Docker `localhost` is the container itself), so it never authenticated and QR pairing generated a
QR pointing at an unreachable `localhost`. Self-hosters running their own relay still override
`RELAY_URL`. Updated `.env.example` accordingly.
### Changed
- Generating a pairing QR (`POST /api/patients/wallet/pair`) now ensures the clinic's relay hub is
connected before pre-registering the request, so the routing is set up even if the connection was
opened lazily.
### Fixed ### Fixed
- **QR "scan to connect" pairing** was broken by the multi-clinic relay routing (v0.8.0): pairing - **QR "scan to connect" pairing** was broken by the multi-clinic relay routing (v0.8.0): pairing
+5 -1
View File
@@ -40,7 +40,11 @@ FRONTEND_PORT=3000
# /hub with its own Ed25519 signing key, so no shared secret is needed. # /hub with its own Ed25519 signing key, so no shared secret is needed.
# RELAY_TOKEN is OPTIONAL/LEGACY — set it only for a private relay that also # RELAY_TOKEN is OPTIONAL/LEGACY — set it only for a private relay that also
# gates on a shared token (then use the SAME value here and on the relay). # gates on a shared token (then use the SAME value here and on the relay).
RELAY_URL=http://localhost:8080 # Defaults to the hosted relay (https://network.temetro.com) when unset, so
# "Join Temetro Network" works out of the box; set RELAY_URL only to point at
# your own relay. Do NOT use http://localhost — inside Docker that's the
# container itself and the relay connection will silently fail.
RELAY_URL=https://network.temetro.com
RELAY_TOKEN= RELAY_TOKEN=
# (Legacy, pre-relay self-hosting.) A phone-reachable URL for the QR when NOT # (Legacy, pre-relay self-hosting.) A phone-reachable URL for the QR when NOT
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "temetro-backend", "name": "temetro-backend",
"version": "0.8.1", "version": "0.8.2",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "temetro backend — Express + Postgres API with Better Auth (email/password, organizations) and org-scoped patient records.", "description": "temetro backend — Express + Postgres API with Better Auth (email/password, organizations) and org-scoped patient records.",
+5 -1
View File
@@ -35,7 +35,11 @@ const schema = z.object({
// its own Ed25519 signing key, so no shared secret is needed. RELAY_TOKEN is // its own Ed25519 signing key, so no shared secret is needed. RELAY_TOKEN is
// now *optional/legacy* — set it only for a private relay that also gates on a // now *optional/legacy* — set it only for a private relay that also gates on a
// shared token (must then match the relay's RELAY_TOKEN). // shared token (must then match the relay's RELAY_TOKEN).
RELAY_URL: z.string().min(1).default("http://localhost:8080"), //
// Defaults to the hosted relay so "Join Temetro Network" works out of the box;
// override only when running your own relay. (A `localhost` default silently
// fails inside Docker, where localhost is the container itself.)
RELAY_URL: z.string().min(1).default("https://network.temetro.com"),
RELAY_TOKEN: z.string().default(""), RELAY_TOKEN: z.string().default(""),
// Public, device-reachable URL of this backend's wallet relay, baked into the // Public, device-reachable URL of this backend's wallet relay, baked into the
// QR a patient scans. Optional — when unset we derive it from the request host // QR a patient scans. Optional — when unset we derive it from the request host
+4 -1
View File
@@ -17,7 +17,7 @@ import {
} from "../middleware/auth.js"; } from "../middleware/auth.js";
import { emitToWallet } from "../realtime.js"; import { emitToWallet } from "../realtime.js";
import { recordActivity } from "../services/activity.js"; import { recordActivity } from "../services/activity.js";
import { expectResponse } from "../services/relay-client.js"; import { connectOrg, expectResponse } from "../services/relay-client.js";
import * as patientService from "../services/patients.js"; import * as patientService from "../services/patients.js";
import { awaitQuickTunnelUrl } from "../services/relay-url.js"; import { awaitQuickTunnelUrl } from "../services/relay-url.js";
import { getNetworkEnabled } from "../services/signing.js"; import { getNetworkEnabled } from "../services/signing.js";
@@ -95,6 +95,9 @@ patientsWalletRouter.post(
); );
// No wallet number to `wallet:send` to yet, so pre-register the request id // No wallet number to `wallet:send` to yet, so pre-register the request id
// with the relay so the scanning device's response routes back to us. // with the relay so the scanning device's response routes back to us.
// Ensure the hub is (re)connected first; if it's still mid-handshake the
// on-auth re-registration of pending requests will catch this one.
await connectOrg(req.organizationId!);
expectResponse(req.organizationId!, view.id); expectResponse(req.organizationId!, view.id);
res.status(201).json({ res.status(201).json({
...view, ...view,
+3 -1
View File
@@ -49,7 +49,9 @@ export function expectResponse(orgId: string, requestId: string): void {
} }
// Open (and authenticate) a hub connection for a clinic, if not already open. // Open (and authenticate) a hub connection for a clinic, if not already open.
// Idempotent — safe to call on startup and again when an org joins the network. // Idempotent — safe to call on startup, when an org joins the network, and
// before generating a pairing QR. The socket auto-reconnects on its own, so an
// existing entry is left as-is.
export async function connectOrg(orgId: string): Promise<void> { export async function connectOrg(orgId: string): Promise<void> {
if (hubs.has(orgId)) return; if (hubs.has(orgId)) return;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "frontend", "name": "frontend",
"version": "0.8.1", "version": "0.8.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "temetro", "name": "temetro",
"version": "0.8.1", "version": "0.8.2",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"shadcn": "^4.11.0" "shadcn": "^4.11.0"