Skip to content

Dial FTW Cloud for relay frames, with the relay as fallback - #87

Draft
frahlg wants to merge 1 commit into
mainfrom
cursor/cloud-carrier-endpoint-3b55
Draft

frahlg wants to merge 1 commit into
mainfrom
cursor/cloud-carrier-endpoint-3b55

Conversation

@frahlg

@frahlg frahlg commented Oct 15, 2026

Copy link
Copy Markdown
Member

What changed

The webapp can send its existing sealed relay session — reads and remote commands, same frames, same /r/<epoch>/<handle>/app join — to FTW Cloud instead of relay.ftw.energy.

  • VITE_CARRIER unset, or anything other than cloud, keeps today's behaviour: dial wss://relay.ftw.energy (VITE_RELAY_URL still overrides that host).
  • VITE_CARRIER=cloud dials VITE_CLOUD_URL or wss://cloud.ftw.energy first. If that socket closes before the host speaks the relay protocol (ready, gone, a frame, or close codes 4400 / 4409 / 4410 / 4429), the carrier dials the relay once and stays there for the rest of that connection. The next launch tries the cloud host again.
  • There is no setting in the UI. Transport stays out of the normal flow.
  • relay/, escrow/, and deploy/DNS are untouched. CSP connect-src now also allows wss://cloud.ftw.energy, which is what lets a flagged build open the socket.
  • api.req uses the same field set as the iPhone app (APIRequest.cbor in srcfl/ftw-app): maxBytes, method, path, then query only when it has entries and with keys sorted, body only when present, stepUp only when true. A caller-supplied maxBytes still overrides the 8 MiB ceiling.

The default stays the relay because cloud.ftw.energy does not serve /r/... yet. A parallel port of this frame format onto srcfl/ftw-cloud is what makes VITE_CARRIER=cloud useful.

API / JSON compared with the iPhone app

Aligned here:

  • api.req field set and query-key order, as above.

Already the same on the wire, left as they were:

  • PUT /api/loadpoints/{id}/schedule: soc, time_of_day_min, time_of_day_min_utc, recurring, days, surplus_unlock_bat_soc
  • POST /api/loadpoints/{id}/vehicle: {capacity_wh}
  • Commands: site.mode.set {mode}, planner.prefs.set {safety_k | battery_export}, loadpoint.hold ({id, power_w, hold_s, phase_mode}, pause {id, power_w: 0, hold_s: 0}, release {id, clear: true}), loadpoint.soc.set {id, soc}, loadpoint.boost, loadpoint.surplus_only.set
  • GET /api/savings/daily?days= reads saved_ore. The iPhone also reads actual_cost_ore from the same object; this app's compact card does not.
  • GET /api/energy/daily?days=
  • GET /api/planner/prefs: safety_k, mapped_k, battery_export, mapped_mode
  • GET /api/status, GET /api/loadpoints, GET /api/mpc/plan

Different, on purpose:

  • Reads vs commands. The iPhone reads at wss://cloud.ftw.energy/v1/app?home=<id> (Noise IK to the service key, CloudWire CBOR; that service can read). Its commands still go to wss://relay.ftw.energy/r/<epoch>/<handle>/app. This app sends both reads and commands as the existing relay frames. VITE_CARRIER=cloud points that join at wss://cloud.ftw.energy. It does not open /v1/app.
  • History. The iPhone chart is GET /api/energy/history?since&until&bucket=1h&scope=system, with points[] of flow, quality, bucket_start_ms, bucket_len_ms, energy_wh. This app's chart is the session hist.query (series, res 5m|1h, fromMs, toMs, tile have, binary columns). The frame port carries hist.query, so the chart stays on it.
  • Cloud grant, iPhone only. This app does not call GET /api/cloud (state, purposes, connected, purgedAtMs), POST /api/cloud/grant (scopes, policy: "ftw-cloud-2026-10"), POST /api/cloud/stats (policy: "ftw-stats-2026-10"), or DELETE of either.
  • hello.app.ua is pwa here and native on the iPhone.
  • hello_ok.cloud (box P-256 key, home id, boot, source and loadpoint names) is parsed by the iPhone for the readable session. This app leaves that map unread.

Still in Frankfurt (eu-central-1)

  • relay.ftw.energy — the relay process, dead-man push rows, and fleet totals. Still the default host and the fallback. DNS and relay/deploy are unchanged.
  • escrow.ftw.energy — sealed recovery copies. Unchanged.
  • Pairing origin app.ftw.energy and the passkey RP ID. Unchanged.

Not Frankfurt: the web app is Cloudflare Pages (wrangler.jsonc). FTW Cloud itself is already in Stockholm (eu-north-1) at cloud.ftw.energy, and it does not yet speak these frames.

Tests

npm run check and npm run build pass. New and nearby tests pass (origin, api.req, relay carrier including fallback, CSP, cloud claims, API passthrough). In this environment the full npm test also fails on main for WebCrypto PRF (importKey on Node 22.14) and the escrow service timing out; those paths are not part of this change.

Open in Web Open in Cursor 

VITE_CARRIER=cloud sends the sealed session to wss://cloud.ftw.energy
and falls back to relay.ftw.energy when that host does not speak the
protocol. The default build still dials the relay. api.req query keys
match the iPhone app.

Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
ftw-webapp de21878 Commit Preview URL

Branch Preview URL
Oct 15 2026, 01:03 PM

@frahlg

frahlg commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

Please pause the carrier part of this. Fredrik decided on 15 October 2026 that the web app, like the iPhone, reaches the box through a tunnel inside its FTW Cloud session, not through the relay's /r/…/app frames on the cloud host (see the latest review comment on srcfl/ftw#1632). Worth keeping, as one small PR: the api.req field alignment with its tests (src/lib/protocol/messages.ts) and the CSP entry for wss://cloud.ftw.energy.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants