Skip to content

Design for the flakiest client in the room (the TV browser)

Here is a thing nobody tells you when you start building with an AI agent: the agent is very good at writing the code, and it will not, on its own, decide what the code has to survive. It will happily build you a clean, reasonable client that works beautifully on your laptop and falls over the first time it meets the worst computer in the house.

In this project, the worst computer in the house is the television.

This post is a worked example of one design move: taking a constraint that sounds like a footnote -- "the TV browser is unreliable" -- and letting it earn a seat at the top of the design, where it gets to dictate the shape of the network protocol and the client. The point is not the TV. The point is the propagation: how one real, un-wishable-away constraint travels from "Tizen drops the socket" all the way to "the server, not the client, owns the truth on reconnect." That propagation is the part an agent will not do for you unless you frame it -- and learning to frame it is most of what learning to design with AI actually is.

The constraint nobody chooses

A board-game night in this product runs on a TV in the living room (the shared board), phones in players' hands (their private views), and maybe a tablet acting as referee. The TV is the social center of the experience. It is also, by a wide margin, the flakiest device connected to the server.

The repo's own agent rules name the constraint flatly. From CLAUDE.md, under "TV browser quirks":

TV browsers (Tizen, webOS, Android TV) drop WebSocket connections frequently and have underpowered CPUs.

Read that as a hardware fact, not a complaint. Smart-TV browsers run on cheap SoCs with a fraction of a phone's CPU, aggressive background-tab throttling, and network stacks that were tuned for streaming video, not for holding a live bidirectional socket open for an hour. They drop. They are slow. You do not get to fix this, because you do not control the device -- the person watching the game bought whatever TV was on sale in 2019, and your job is to be good on that.

Once you accept that you cannot change the device, a short list of behaviors stops being stylistic choices and becomes forced consequences. The same CLAUDE.md section spells them out, and every item is downstream of "flaky and underpowered":

  • No hover states. A TV has no pointer. Hover-driven UI is not a preference here; it is unreachable. (The architecture leans into this: the TV's role, TABLE, is deliberately non-interactive -- see the overview docs, "Client roles".)
  • Aggressive reconnect. Because the socket drops often, the TV client retries faster than the others -- roughly every 3 seconds versus 1 second elsewhere. That asymmetry is documented in PROJECT_BRIEF.md under "Reconnection," and it exists only because this one device class drops more.
  • Large fonts for 10-foot viewing. Nobody reads a TV from 18 inches. The font scale is a function of viewing distance, which is a function of the device being a TV.
  • Simplified animations. The CPU cannot afford the phone's animation budget, so it does not get one.
  • It does not hold move history in memory. A 200-move game history would make an underpowered TV swap. So the TV client keeps the current state and, at most, the last move or two for animation. History lives on the server.

None of these is a "TV theme." Each is the only correct answer to a hardware limit you did not get to vote on. That is the tell of a real constraint: it removes options rather than adding them.

How the constraint shaped the protocol

Here is where the propagation gets interesting, and where the design reasoning earns its keep.

Take the last item seriously -- the TV cannot be trusted to remember anything -- and follow it upstream. If a client drops every few minutes and cannot hold history, then a protocol that depends on the client remembering where the game was is a protocol that breaks several times per game. So the design cannot put the client in charge of state. The constraint forces the server to be the single source of truth, and forces reconnection to be a re-sync from the server, not a resume from the client.

That is exactly the shape this project's protocol takes. The rules in PROJECT_BRIEF.md ("Load-bearing architecture rules") are blunt about it:

Server is authoritative. Clients are untrusted. State flows one direction: server computes -> role-filtered view -> client renders.

So when the TV's socket drops and the client reconnects three seconds later, the recovery is almost boringly mechanical: the client re-attaches, and the server re-sends the current role-filtered view. The TV does not say "I think the game was on move 47, catch me up from there." It cannot, and it is never asked to. It says "I am back," and the server hands it the present. There is no client-computed state to reconcile, because the protocol has no way to express "the client believes the state is X." This is the same one-direction discipline the four-role WebSocket protocol post walks through in detail -- the TV's flakiness is one of the concrete pressures that makes "never round-trip client-computed state" a rule worth enforcing rather than a slogan.

Notice what the constraint bought us. A design built to survive the worst client ends up safer for every client. "The server owns truth and re-sends on reconnect" was forced by the TV, but it is also what makes a player's phone reconnect silently after they walk through a dead spot in the house, and what makes the whole system resistant to a malicious client lying about state. One device class's weakness hardened the protocol for everybody.

There is a recovery-design lesson hiding in this too, and it is worth naming because beginners get it backwards. The interesting question when something fails is not "how clever does my recovery code need to be?" It is "how much of this can I make mechanical so that nothing has to be clever?" A dropped TV socket is a failure, but the response to it carries zero judgment: re-attach, re-send the view, done. That deliberate split -- which parts of failure handling are dumb-deterministic and which parts genuinely need a decision -- is the through-line of the companion post on what is mechanical and what is the LLM when a stage fails. The TV taught the client side the same thing that post teaches about the orchestrator: push as much of "recovery" as you can down into machinery that cannot be wrong.

Telling the truth about the connection

A protocol that expects drops needs a UI that expects them too. If the link is flaky and you pretend it is solid, the user is left staring at a frozen board wondering whether the game died or their cousin is just thinking.

So the client does not pretend. The reconnection design surfaces connection state honestly, with what PROJECT_BRIEF.md and the overview docs (Section 5, "Disconnect handling") call a three-signal pattern -- three coordinated cues on each participant's roster row:

  • Avatar opacity -- full, fading as the connection degrades.
  • A status dot color -- green when connected, stepping through amber and orange to gray.
  • Metadata text -- a plain-language label that moves through connected -> reconnecting -> away -> disconnected.

Three signals for one fact, because the fact matters and because redundancy reads at a glance from across a room. The reason this surface exists at all is the constraint: in a world where the TV never dropped, you would not bother building a careful four-state connection indicator. In this world, "is everyone actually still here?" is a question the table asks several times a game, and the UI answers it without anyone having to guess. Designing the flaky case as a first-class, visible state -- rather than an error to hide -- is the direct UI consequence of taking the constraint seriously.

The forward edge: async inverts the whole model

Now the part that shows why this kind of design pays compound interest.

Everything above treats a disconnect as a transient problem to recover from quickly -- the TV drops, we re-sync in seconds, play continues. But the roadmap has a phase where that assumption flips entirely. the overview docs, Phase 10, introduces async play and the game Rail Routes, the first game in the catalog where async is the dominant mode rather than the exception. In an async game, a player takes their turn, closes the app, and comes back hours later. As the roadmap puts it:

"Disconnected for 6 hours" is normal in async.

A six-hour gap is not an error to recover from; it is the expected rhythm of the game. The whole sync-era disconnect timeline -- detect, grace period, resolve -- simply does not apply, and Phase 10 replaces it with a different one built around turn deadlines and reminders rather than reconnection.

Here is the payoff. The instinct that handled the flaky TV -- do not trust the client to remember; the server owns the truth and re-sends it whenever a client reappears -- is exactly the instinct that makes async tractable. A client that comes back after six hours is, architecturally, the same problem as a TV that comes back after three seconds: a client showing up with no reliable memory of the game, asking the server for the present. The design that was forced on the project by a cheap television turns out to be the foundation a fundamentally different play mode stands on. You do not rebuild for async; you discover that the discipline the worst client already demanded was the discipline async needed all along.

That is not luck. It is what happens when you let a genuine constraint drive the architecture instead of papering over it: the resulting shape tends to be the right shape for the next hard thing too, because real constraints push you toward designs that assume less and verify more.

The lesson for building with an agent

If you take one thing from this, take the method, not the TV.

An AI agent will build whatever you point it at, and it will build it competently. What it will not do -- what it cannot do, because it requires a judgment about your world that is not in the prompt -- is decide which constraint gets to be load-bearing. Left unframed, it will optimize for the happy path, because the happy path is what most training data looks like. The design work that is still yours is naming the hostile constraint out loud, early, and then insisting that it propagate: from the device, to the protocol, to the UI, to the phases you have not built yet.

Concretely, the move looked like this:

  1. Name the worst client honestly. Not "support TVs" but "the TV drops constantly and is too weak to remember anything." A constraint stated as a weakness removes options; a constraint stated as a feature ("TV support!") hides them.
  2. Follow it upstream until it hits the architecture. "Cannot remember" forced "server owns truth," which forced "reconnect is a re-sync, not a resume," which forced "state flows one direction." Each step is a small, defensible inference -- and writing those inferences down is the prompt the agent actually needed.
  3. Make the failure case a visible, first-class state, not an error to hide -- the three-signal connection indicator exists because drops are normal.
  4. Notice what the constraint bought you elsewhere. The flaky-TV discipline is the async foundation. Good constraint-driven designs tend to generalize for free.

The TV is the worst computer in the house, and that is the most useful thing about it. Design for the client that punishes your assumptions, write the propagation down so your agent can follow it, and the easy clients -- and the hard future ones -- mostly take care of themselves.