Case study

ReadThat 12: The Web Client

4 min read✦ AI generated with human guidance & review

The React + TypeScript PWA: same-origin Cloudflare Workers serving, IndexedDB offline cache and outbox, an allowlisted SDUI renderer sharing the wire contract with the apps, adaptive HLS, and a service worker with a deliberate caching policy.

Part 12 of the ReadThat case study.

📲 Try it live: open the web client in any browser, or download the Android APK.

The web client is the third front end on the same backend, and it is deliberately React 19 + TypeScript, not shared Kotlin. Part 11 makes the argument; this page shows the result: a production PWA that shares the contracts with the apps (the SDUI wire model, the API, the telemetry vocabulary) while staying on the toolchain the web is optimized for.

The web front page: server-driven feed rendered from versioned SDUI cells, PWA install prompt, communities rail A 250-comment thread rendered from the typed comment model

Left: the server-driven front page, adaptive three-column layout. Right: a 250-comment fixture thread through the same typed comment contract the apps use.

Tech and framework

PieceChoice
UIReact 19 + TypeScript, React Router 7
BuildVite; tsc -b type gate; Vitest for logic/component tests
ServingCloudflare Workers Static Assets, same origin as the API
OfflineIndexedDB: sessions, D1 bookmarks, feed pages, post details, mutation outbox
VideoNative HLS on Safari; lazy-loaded hls.js elsewhere
App-nessInstallable PWA; service worker with an explicit caching policy

Architecture

The deployment shape does a lot of quiet work: the compiled app ships from the same Cloudflare Worker origin as /v1/* and /health (part 6), so auth, D1 session bookmarks, the service worker, and browser security policy never depend on cross-origin exceptions. One npm run deploy from the backend builds the client and uploads hashed immutable assets plus Worker code together.

The client’s data policy mirrors the Android architecture in browser primitives:

  • Render cached, refresh behind. Feed pages use the same signed cursor pagination as the apps, driven by an intersection-observer sentinel; cached IndexedDB pages render immediately while the network refreshes in the background.
  • Offline-first writes. Authenticated mutations go through an account-scoped outbox in IndexedDB and replay on reconnect: the same durable-intent pattern as the Android outboxes (part 5). Permanently failed commands stay inspectable instead of vanishing.
  • Read-your-writes. D1 bookmarks persist client-side and ride every request, so the consistency contract from part 6 holds on the web too.

SDUI integration

The feed is the same server-driven contract the Android app consumes (part 2), rendered through an explicit cell allowlist: the TypeScript renderer maps known cell types to components and drops unknown types safely, the web twin of the Kotlin Unknown cell. Post detail and comments stay a typed recursive domain model, exactly where the Android client draws the same boundary. One ranking, one wire format, three renderers, and the where-SDUI-stops decision holds on every platform.

The service worker, deliberately bounded

Service-worker caching is where PWAs quietly hoard storage or serve stale poison, so the policy is explicit:

  • Precache only the small app shell; hashed assets are immutable by construction.
  • Runtime-cache same-origin images and immutable video segments under quota-aware LRU limits.
  • Never cache range responses or HLS/DASH manifests (dynamic, the same rule as the Media3 cache in part 3); signed cross-origin image URLs stay on the browser network path.
  • Don’t precache the lazy HLS parser: Safari never needs it.

Playback follows the mobile discipline: only the most visible video plays, nearby media attaches conservatively, and navigation uses native view transitions with full reduced-motion respect.

What the web page proves

The claim in part 11 was that a high-scale web client should share contracts, not runtime. This is that claim running: feature-complete (registration, profiles, communities, posting, nested comments, voting, search, infinite feed), offline-capable, installable, and fast on the toolchain the web ecosystem actually optimizes, while every byte of ranking, ACL, and wire policy stays shared with the apps.


← Part 11: Kotlin Multiplatform · Back to the series hub →