Web — Cloudflare
SPA on Cloudflare Pages → fetch('/api/*') → Cloudflare Worker (Hono) on api.restura.dev → upstream. Same-origin, no CORS friction.
Restura ships from a single React renderer to web, desktop, and self-hosted targets. The transport layer is the only thing that differs — chosen at runtime by isElectron() in src/lib/shared/platform.ts.
Web — Cloudflare
SPA on Cloudflare Pages → fetch('/api/*') → Cloudflare Worker (Hono) on api.restura.dev → upstream. Same-origin, no CORS friction.
Self-hosted — Node + Docker
One Node process running the same Hono app that runs in the Worker. Serves both the SPA and /api/* on one port. Native WebSocket and CONNECT proxy adapters.
Desktop — Electron
SPA loaded via file:// → IPC over window.electron → Electron main process → Node http/https/net/tls. Native capabilities (mTLS, SOCKS, Kafka).
The two HTTP backends (Cloudflare Worker and Node/Docker server) share a single Hono app via the createApp(deps) factory in worker/app.ts. Each entry supplies its own adapters for the platform-specific bits (CONNECT proxy, native WebSocket).
flowchart TB
subgraph Web["Web target"]
SPA1[React SPA<br/>Cloudflare Pages]
SPA1 -->|fetch /api/*| W[Cloudflare Worker<br/>api.restura.dev]
end
subgraph SH["Self-hosted target"]
SPA2[React SPA<br/>same bundle]
SPA2 -->|/api/*| N[Node + Hono<br/>same createApp]
end
subgraph DT["Desktop target"]
SPA3[React SPA<br/>file://]
SPA3 -->|IPC| EM[Electron main]
end
W --> U[(upstream)]
N --> U
EM --> U
classDef target fill:#7b6ef622,stroke:#7b6ef6,color:inherit;
class Web,SH,DT target;
Each protocol (HTTP, gRPC, MCP, SSE, WebSocket, AI) is implemented once as a backend-agnostic orchestrator in shared/protocol/. Each backend supplies a thin Fetcher adapter — and that’s it. Everything else — SSRF validation, header sanitisation, body construction, response shape, gRPC status mapping, SSE / NDJSON parsing — lives in shared/protocol/ and runs identically across Worker, Node, and Electron.
flowchart TB
SP["shared/protocol/<br/>{http,grpc,mcp,sse}-proxy.ts<br/><br/>validation · body · headers · response shape"]
SP -->|Fetcher| W[worker/handlers/*<br/>globalThis.fetch]
SP -->|Fetcher| E[electron/main/*-handler.ts<br/>undici / Node net]
SP -->|Fetcher| C[cli<br/>undici]
classDef core fill:#7b6ef6,stroke:#4f46e5,color:#fff;
classDef adapter fill:#6366f122,stroke:#7b6ef6,color:#7b6ef6;
class SP core;
class W,E,C adapter;
See Shared protocol layer for the deep dive.
createHashRouter is used so the renderer works under both https:// (Pages) and file:// (Electron). There is no server-side routing.
All global state lives in Zustand stores with the persist middleware. Stores are validated with Zod schemas in src/lib/shared/store-validators.ts.
src/lib/shared/dexie-storage.ts (IndexedDB via Dexie).src/lib/shared/secure-storage.ts (encrypted electron-store via IPC; key wrapped by Electron safeStorage → OS keychain).Secret-bearing fields use the SecretRef handle pattern — see ADR 0007 — so plaintext never enters the Zustand store, the persistence layer, or exported collections.
src/lib/shared/capabilities.ts is the single source of truth for what works on web vs. desktop. It feeds:
npm run capabilities:check) that fails the build if the doc drifts from the code.