Someone else's phone

The first production deploy, a snake that moved without appearing to, and what the first real players found within a day.

Day one ended with 60 passing tests and a game that had never left my machine. The twelve commits that followed, from Monday evening to Tuesday midday, are the record of what those tests did not cover: a real server, a real phone, and real players.

The server disagreed first#

Getting it to run at all meant finding the assumptions my machine had been hiding. The database driver went first: better-sqlite3 is a native binding with no business on the host, so the runtime switched to a libsql client, and since the new driver is async where the old one was synchronous, every database call in the API routes and the daily seeder gained an await. Then the migrations could not find themselves, because resolving their directory relative to import.meta.url points somewhere inside the bundled server output where no migration files exist; process.cwd() is the project root in both environments, so it won. Neither of these is a bug in the game. They are the class of assumption you cannot catch on the machine that formed them, and no amount of unit testing would have surfaced either.

The snake that would not move#

The deployed game revealed a better one: the snake did not visibly move. The engine processed each word correctly, the animation queue played its steps on schedule, and the board stayed frozen until the last step, at which point the snake teleported by the length of the word. Every engine test passed while this happened, because the engine was correct. The bug lived in the thin reactive layer binding engine to screen, which is exactly the strip of code that day one’s framework-free purity had left without tests. The fix rebuilds the grid on every animation step, and its own comment is the whole diagnosis:

// Rebuild the grid from the new snake positions
// and edibles. This is what GameBoard renders —
// without this the board never updates
// mid-animation.
const newGrid = rebuildGrid(

Watching the snake actually walk a word for the first time was the moment the game started existing.

Everything before that was a data structure with opinions.

What the first players found#

Then the first real players arrived, and within hours they had found what I had not. None of their finds lived in the engine; every one sat in the bindings and seams around it, the same territory as the invisible snake.

Two same-night corrections were less about bugs than about honesty. The alphabetical-streak combo accepted any three words in dictionary order (“apple, mango, zebra” qualified), and now requires consecutive first letters, A then B then C. And the share text lost its emoji mini-grid, because compressing a 20 by 20 board into 10 cells collapsed most games into a single column that conveyed nothing. Day one had been proud of that grid; score and word count communicate more.

Where this leaves things#

By Tuesday morning the fixes had grown into deliberate UX work: a navigation guard with a real confirmation dialog, a cap on words per daily challenge, a guarantee that an apple is always on the board, wider high-contrast coverage, and a snake outline that follows the body’s contour. Both visual changes needed a second pass the same morning, and the last apple-respawn fix was a deletion of the earlier one, which is worth noticing as a pattern this early: the second fix to the same bug is usually a deletion. Everything in this range still went straight to main, unreviewed. With real players now finding real bugs within hours, that habit is living on borrowed time.