The shape of it — architecture without code

Before any of it gets built, it is worth seeing the whole thing on one page. Five moving parts, each chosen for a reason you can argue with. If you disagree with a choice here, you will build something better later — but you will know what you are trading away.

The five parts

  browser / phone            an LLM assistant
        |                          |
        |  HTML + fetch            |  MCP tools  /  OAuth
        v                          v
  +-----------------------------------------+
  |     Cloudflare Pages  (static files)     |   index.html, dashboard, story page
  |     Cloudflare Functions  (/functions)   |   the API + middleware + MCP server
  +-----------------------------------------+
                      |
                      v
                +------------+
                |     D1     |   one table per data domain
                +------------+

That is the whole system. No framework, no bundler, no build step, no server you have to keep alive.

1 · Zero-build static site

The dashboard is an index.html. The story page is a product.html. You edit a file, push, and Cloudflare serves it from the edge seconds later.

Why: every build step you add is a thing that can break between you and seeing your change. On a project whose purpose is learning, that feedback loop is the most valuable asset you have. A bundler buys you very little here and costs you the loop.

What it costs: no components, no JSX, hand-written HTML. Above a certain size that hurts. This project is below that size — and noticing when you cross the line is itself a lesson.

2 · Functions as the backend

Anything that is not a file is a Pages Function: a file under functions/api/… becomes a route automatically. functions/api/health/daily.js answers /api/health/daily.

One special file, functions/_middleware.js, runs before every request. That is where authentication lives — in one place, not sprinkled across twenty handlers.

The single most useful structural idea in this whole level: if a rule must hold for every route, it belongs in middleware. A rule that each route has to remember to apply is a rule that will eventually be forgotten by exactly one route, and that is the one an attacker finds.

3 · D1 as the only datastore

One SQLite database at the edge. One table per data domain — measurements, meals, workouts, sleep sessions, notes — rather than one giant events table with a type column.

Why separate tables: a weight and a sleep session have almost nothing in common. Forcing them into one shape means every query starts with a filter and every column is nullable. Separate tables let the schema say true things.

4 · An MCP server, so the agent has tools

Without this layer, "log my weight with an assistant" means: you tell the assistant, it writes some JSON, you copy the JSON, you paste it into a terminal. That is not automation, that is you being a courier.

MCP — Model Context Protocol — lets you expose named tools: log_weight, log_meal, get_wellbeing. The assistant calls them directly. You say "78.4 this morning" and the row exists.

The same surface is documented as ordinary REST in an openapi.yaml, for anything that does not speak MCP. One backend, two descriptions of it.

5 · Two authentication paths, one set of tools

CallerHow it authenticatesWhy
A chat assistant you connect as a clientOAuth 2.1, scopedIt is a third party. It should get a revocable, limited grant — not your master key.
Your own agent / scriptsStatic bearer tokenIt is you, on your machine. A full OAuth dance buys nothing and costs a lot of moving parts.

Both arrive at the same check in middleware and the same tools behind it. Two doors, one room.

The part most people skip: a public tier that is not a lie

A dashboard nobody can see is not much of a dashboard, and a health project has genuinely different sensitivities inside it. The pattern here is an allowlist of GET endpoints that are public — and those endpoints return real but redacted data.

Note what this is not: it is not "hide the page". The page is public and the numbers on it are true — there are simply numbers it never asks for. The redaction happens on the server, not in the browser.

Why that distinction matters more than it sounds. "Hidden in the UI" is not privacy. If the endpoint returns the field and the page merely does not draw it, the field is public — anyone who opens the network tab has it. The only redaction that counts is the one where the data never leaves the server.

What this costs you

Honestly: edge functions have short execution limits, D1 is SQLite with SQLite's concurrency story, and a static site means you write HTML by hand. If you wanted a hundred thousand users, you would choose differently. You want one user, on five devices, for years — and for that, this shape is close to ideal and costs about nothing to run.

Where next