Skip to content

System architecture

The one-sentence version

Everything — the API, the website, the database, file storage, caching, and the AI — runs inside one Cloudflare Worker at the edge, reachable at a single domain. There is no server to patch, no database host to scale, and no separate frontend deployment.

The whole system in one diagram

graph TD
  U["Browser / Mobile<br/>(Manager · Teacher · Student)"] -->|HTTPS| DOM["kategeneral.long-cao.dev"]
  DOM --> W["Cloudflare Worker<br/>(single deployment)"]

  W -->|"/ (web app)"| SPA["Static assets<br/>React single-page app"]
  W -->|"/api/*"| API["API layer<br/>(Express on Workers)"]
  W -->|"/media/*"| SIGN["Signed-URL media gateway"]

  API --> D1[("D1 database<br/>SQLite at the edge")]
  API --> KV[("KV cache<br/>fast key-value")]
  API --> AI["Workers AI<br/>grading · tutor · transcription"]
  SIGN --> R2[("R2 storage<br/>audio · uploads")]
  API --> DO["Durable Object<br/>background OCR jobs"]
  DO --> OCR["kate-ocr service<br/>PaddleOCR container"]
  API -. optional .-> AZ["Azure Speech<br/>pronunciation"]

The request flow

When a request hits the Worker, it is routed by URL:

Path Handled by Notes
/api/*, /health The API layer (Express-on-Workers) All business logic and data access
/media/* The media gateway Streams audio/files from R2 only with a valid signed URL; supports range/seek for players
everything else Static assets The React app (single-page routing), served with security headers

Why this shape

  • One deployment. The frontend build is bundled into the same Worker that serves the API. A single deploy command ships both. There is no CORS, no separate hosting, no environment drift between web and API.
  • Edge-native. D1 (database), R2 (files), KV (cache), and Workers AI are all Cloudflare primitives the Worker binds to directly — no connection strings to external hosts, no network hops off Cloudflare.
  • Same-origin everything. The web app calls /api/... on its own origin, so there is nothing to configure per environment.

Background & heavy work

Two things can't finish inside a normal web request, so they run out-of-band:

  • OCR test extraction (~60s). The request returns a job ID immediately; a Durable Object runs the OCR + structuring on a durable timer and the browser polls for the result. If the work is interrupted it fails cleanly rather than silently double-charging the AI budget.
  • Writing grading runs asynchronously after the essay is saved; the client polls for the result. Grading is idempotent (safe to trigger more than once).

Performance & caching

A KV cache sits in front of hot, read-heavy endpoints (dashboard, question-bank lists, student profiles, materials). It is:

  • Fail-open — any cache problem falls through to the database; nothing breaks.
  • User-scoped — cache keys include who is asking, so no one sees another user's cached data.
  • Self-invalidating — a version token is bumped on every relevant write, so a change is reflected on the next read (within a short TTL).

The three code surfaces

Surface What it is Deployed?
Cloudflare Worker The production app (API + web + AI). This is what users use. ✅ Yes
Legacy microservices An earlier Node.js + MongoDB implementation of the same domain, kept for reference and local development. ❌ No
Platform monolith A composition of the legacy services into one process for offline development. ❌ No

For product purposes, only the Cloudflare Worker matters — the other two are development history and are not part of the live system. See Status & gaps for the recommendation to archive them.