Architecture

A dozen small web apps, each one file, sharing a single backend. No framework, no build step, no dependencies. Here is how it fits together and what that choice costs.

The shape of it

Every app on this site is one .html file containing its own markup, styling and behaviour. Apps that need to remember something call a single PHP endpoint, which talks to one database. That is the whole system.

Request flow on davenn.com A browser loads a single self-contained HTML file. That page calls api.php, which reads and writes one MySQL database and, for some features, calls out to the Anthropic API, Twilio and ESPN. Browser one page, one app appname.html HTML + CSS + JS self-contained api.php every app, every endpoint MySQL one database Outside services Claude ยท Twilio ยท ESPN fetch served exactly as written โ€” nothing compiles it
One page, one file. The only shared server-side component is api.php.

One file per app

There is no bundler, no package manager and no node_modules. What sits in the repository is byte-for-byte what the browser receives.

This is unfashionable, and for a project this size it is the right trade. Each app can be opened, understood and changed on its own, without a toolchain standing between the source and the running page. A change to the cribbage scorer cannot break the flight tracker, because they share no code to break. When one of these apps goes untouched for a year, it still runs โ€” there is no dependency tree quietly rotting underneath it.

The cost is real: styling and small utilities are duplicated across files rather than shared. That duplication is accepted deliberately. Extracting it into common files would buy tidiness and give back the independence that makes the whole arrangement work.

One file behind them

All server-side behaviour lives in a single PHP file. Requests select an operation by query parameter and always get JSON back:

GET /api.php?action=cp_week&season=2026&week=3

Internally that is a flat sequence of guard clauses, not a router. Each one checks the method and the action, does its work, emits JSON and exits. It is the least clever structure available, which is the point โ€” the path any request takes can be read top to bottom with no indirection.

Tables

Each app's tables carry a short prefix, so one database serves everything without name collisions: tb_ for the tool library, cp_ for the pick pool, bg_ for glucose readings, and so on. A handful of older tables predate the convention and keep their original names, because renaming a live table to satisfy a pattern is a bad trade.

Schema is created on demand rather than migrated. The table definitions run on every request and do nothing if the tables already exist, so a new table appears the first time the code that needs it is deployed. Changing a column on an existing table is a deliberate manual step.

Look and feel

The apps are visually consistent by convention rather than by shared stylesheet. Each file defines the same small set of colour tokens for light and dark, and the same rule for choosing between them: a stored preference wins, otherwise follow the operating system.

That choice is applied in a tiny inline script in the document head, before any other styling is parsed, so a page never paints in the wrong theme and then corrects itself. It is a few lines duplicated in every app, and worth it โ€” a flash of white at 6am is the kind of small insult that makes something feel unfinished.

Status colours are never themed, and never carry meaning alone. Win and loss, up and down, good and bad are always paired with a word or a shape as well as a hue. Red and green are exactly the distinction the most common form of colour blindness cannot make.

Installing an app

Most of the apps can be installed to a home screen. Each has its own manifest and icon, and one service worker serves all of them.

Its caching rule is split by kind. Pages are fetched from the network first and fall back to the cached copy only when offline, so an installed app is never stuck on a stale version of itself. Fonts, icons and other static files are served from cache first, since they change rarely and loading them twice helps nobody. Calls to the API are never intercepted at all โ€” cached data that looks live is worse than no data.

Counting visits

Which apps actually get used is measured in-house rather than by an analytics service. Each page sends one small, anonymous page-view count back to the same API when it loads, and the API adds a coarse location by looking the visitor's address up in a geolocation database kept on the server itself. The address is used for that lookup and then dropped. Nothing about a visit leaves the site.

Unique visitors are counted without cookies: a one-way code is derived from each request using a random value that is thrown away every day, so a visitor can be counted once per day but never followed from one day to the next. The same API also keeps a running tally of every request it serves, per operation, with failure counts and timings, which is how a slow or failing endpoint shows up before anyone reports it.

Shipping a change

Pushing to the main branch is the deploy. Continuous integration checks the PHP for syntax errors, refuses to publish if anything fails to parse, and then uploads only the files that actually changed.

A second job checks that the API reference still matches the code it describes. That page is generated from the source rather than written by hand: the description of each endpoint lives as a comment beside the code that implements it, and a script turns those into both an in-repository reference and the public page. If the two fall out of step, the build says so. It does not block a deploy, though โ€” broken code should stop a release, and lagging documentation should not.

There is also a changelog. Adding an entry to it is what notifies anyone who has signed up for updates, which makes writing one a deliberate act rather than a formality.

Outside services

ServiceWhat it does here
Claude Reads photographs. A picture of a tool becomes a catalogue entry; a photographed pick sheet becomes a set of picks, shown for correction before anything is saved.
Twilio Sends update notifications by text, and receives the messages that let a pick sheet be submitted by photo.
ESPN Scores and schedules for the football pool. An undocumented public feed, so it breaks without warning and the code assumes it will.

These are described in more detail, from a data-handling point of view, in the privacy policy.

What this is not

This arrangement suits a personal site maintained by one person, where the apps are small, independent and mostly finished. It would be a poor choice for a team, for anything with a shared component library, or for an application whose parts genuinely need to know about each other.

The honest summary: the duplication is the price, and independence is what it buys. For twelve small apps that each want to keep working untouched for years, that is a price worth paying.