Serving today's puzzle
Three days of stale PWA caches, a Turso migration table that rejected its own tracking schema twice, and the first real players finding their way in.
The gate from the day before checked code before it reached main; it checked nothing about the database underneath, or about the people who had started actually playing on it. Most of what closed those gaps over the next three days did not come from hand-branched feature work: nine of the range’s eleven pull requests were GitHub’s own coding agent working a queue of filed issues under branch names it chose for itself. What it found waiting there was a Turso instance that rejected its own migration table twice, a dictionary that existed on paper but not in production, and the first real players who needed the game to explain itself before it asked them to type a word.
Yesterday’s cache, reloaded#
An installed “Add to Home Screen” copy of the game kept reopening to yesterday’s build, wrong bundle URLs and all, because SvelteKit’s content-hashed JS and CSS already carry immutable cache headers but the HTML document that references them did not; the fix is one conditional in the request hook, so the shell always asks again while the hashed assets underneath stay cached indefinitely. A companion fix the same evening made switching language on the free-play page start a fresh game immediately, mirroring a pattern the daily challenge already needed: state that depends on a setting has to know when the setting changes.
if (response.headers.get('content-type')?.startsWith('text/html')) {
response.headers.set('Cache-Control', 'no-store');
}
A migration table Turso would not accept#
Render deployments began crashing on startup against the table Drizzle’s automatic migrator uses to track which migrations have already run, created with a SERIAL PRIMARY KEY, valid Postgres and nothing Turso’s SQLite dialect will accept, so every deploy failed before the app served a single request. The fix replaced the automatic migrator with a small hand-written one; a second, unrelated command needed the identical fix, because pnpm db:migrate, used to stand up a database from scratch, still called drizzle-kit migrate directly and hit the same error, until a thin script routed it at the same hand-written runner so there was exactly one way to apply a migration, not two.
-- was: id SERIAL PRIMARY KEY (Postgres-only, rejected by Turso)
-- now: id integer PRIMARY KEY AUTOINCREMENT NOT NULL
The tables existed, the words did not#
A database migrated cleanly for the first time, and still rejected every word a player typed, because build-dictionary.ts had only ever written to a local SQLite file through better-sqlite3, a driver with no path to a remote Turso URL; production had the schema and none of the roughly 370,000 words meant to fill it, and the daily challenge quietly cached that day’s empty word set on its first run, which then stayed empty even once the dictionary existed, because nothing had told the seeder that an empty result should be retried rather than remembered. push-dictionary.ts closed both gaps at once, built on the same loader and enricher functions as the local build and aimed at any libsql-compatible target, local file or Turso URL alike, alongside a change so an empty word set is never cached in the first place.
The tables existed. The words did not.
Teaching the first players#
With the database behaving, the rest of the range went to the players who had started arriving: a how-to-play modal for first-time visitors, its close button and body text sized to WCAG’s touch-target and readability minimums; score moved out of the shared toolbar into each mode’s own header; and best word, the single highest-scoring word of a session or a period, tracked and surfaced everywhere the score already was. A late fix in the same range taught client-side word validation to accept æ, ø, and å again, Norwegian’s own letters rejected by a preflight check that still assumed plain ASCII.
Where this leaves things#
One deliberate subtraction closed the range: the automatic PR preview infrastructure, added the day before as a per-PR Turso database cloned from a seed and provisioned by a GitHub workflow, came back out again after its own database connectivity problems made it cost more than the single production setup it was meant to protect. Three days after the first deploy, the lesson is the same one that day taught, restated three more times: whether it is a cache header, a migration path, or a dictionary push script, a daily game is only as good as its ability to reliably serve today’s puzzle.