Dustav.com

Essays

Boring on purpose

The default stack for an AI-built app is the fancy one. I went the other way — server-rendered, no framework, a database older than the framework wars — and it's not a compromise. It's the optimal call.

Point an AI coding agent at a blank directory and ask it to build a web app, and you can guess the stack it'll reach for, because it's the one every tutorial and every "build X in a weekend" thread reaches for: Next.js, React, TypeScript, a component library, an ORM, a serverless host, a managed everything. The modern default. The vibe-coding starter kit.

pal.fun is none of that. It's Node and Express, server-rendered Handlebars templates, plain JavaScript with no front-end framework, and MongoDB. The fanciest thing in the whole stack is the language model. Everything else would have looked unremarkable in 2014.

That wasn't nostalgia, and it wasn't me being unable to do the fancy version — the agent will happily write any of it. It was a deliberate call, and it's the right call precisely because an agent is writing the code. The conventional wisdom flips in the AI era. Here's why.

The bottleneck moved

When you type every line yourself, a powerful framework is leverage: it writes the boilerplate so you don't have to. The entire pitch is "less code to type."

But I don't type the code. So that leverage is worth almost nothing to me — the agent produces the boilerplate either way, fancy or plain, in the same handful of seconds. What a framework doesn't save me is the thing that's actually scarce now: understanding what was written. Cheap production didn't make comprehension cheap. If anything it made it more precious, because now there's a great deal more code, written fast, that I still have to be able to read, trust, and debug.

So the question I care about isn't "what lets the agent write the least code." It's "what lets me — and the agent, in its next cold session — understand the code with the least overhead." The answer to that question is boring.

Magic is where fluent-and-wrong hides

A heavy framework is a tower of helpful magic: implicit data fetching, server-versus-client boundaries, hydration, an ORM that turns method chains into queries you never see, a build step that transforms your code into something else before it runs. Every one of those is a layer where an agent can produce code that looks right, compiles, and is subtly wrong — and where figuring out why it's wrong means understanding the framework's internals, not just your own code.

An agent is dangerously fluent, and fluent-and-wrong is the failure mode I spend most of my judgment guarding against. The more magic sits between "what the code says" and "what actually happens," the more room that failure has to hide. Boring code fails in boring ways: locally, obviously, where you're already looking. A server-rendered Handlebars page either has the data or it doesn't. An Express route does exactly what its lines say, in order, with no hidden lifecycle to misunderstand. When the whole stack is legible, the agent's mistakes are legible too.

Owning every layer

A boring stack is one a single person can hold a complete mental model of. When something breaks, the failure is somewhere in my code — not in the interaction between four abstractions maintained by other people, on versions that quietly shifted last month. I'd rather own every layer than inherit a runtime whose internals I'd have to go learn to debug at 2am.

It cuts the other way too: the agent can read the entire path end to end, because the entire path is there, in the repo, not buried in a dependency. Each new session re-derives its understanding by reading the code — and the less of the real behavior that's hidden inside node_modules, the more of it the agent can actually see and reason about. Boring code is more agent-legible, not less. There's also just more of it in the world, and so more of it in the model's training; an agent writes more reliable code in a decade-stable, well-trodden stack than on whatever shipped last quarter.

A note on JavaScript, not TypeScript

In fairness — since the rest of this is a story about deliberate calls — plain JavaScript over TypeScript wasn't one of them. I write JavaScript because it's what I learned a long time ago. That's the whole reason. TypeScript is probably the better tool: types catch a class of mistakes before the code even runs, which is exactly the kind of guardrail you'd want with an agent producing code at speed. Whether it actually helps the agent much, I honestly don't know — and in practice it hasn't seemed to matter here. The fluent-and-wrong mistakes get caught in review and in the running product either way. So there's no principled stand in this one: I picked what I knew, the seams haven't shown, and I wouldn't talk anyone out of reaching for TypeScript instead.

It's the same stance as the rest of the product

None of this is separate from what pal.fun is. The whole product runs on the bet that legibility beats magic — that an AI you can read all the way down is more trustworthy than a clever black box. It would be incoherent to make that argument about the agent and then build the agent on a stack I couldn't see into myself.

The boring stack pays a second dividend, too: a small, server-rendered app with a strict content-security policy and zero third-party client-side dependencies — every script, style, font, and icon the browser runs is first-party, served from us — has a tiny attack surface next to a framework app dragging a thousand transitive packages into the browser. Secure by construction is a lot easier when there's simply less construction.

The flip, stated plainly

So here's the counterintuitive thing I'd hand to anyone building this way: the more capable the agent, the more boring your stack should be. Not despite the AI — because of it. The agent erases the cost that fancy frameworks exist to erase, and it adds a cost — comprehension at speed, fluent mistakes to catch — that fancy frameworks make worse. Pick the stack that's easiest to understand, audit, and own end to end, and turn the absurdly capable hands loose writing a lot of plain, readable code in it.

Boring on purpose. It ages well, it breaks honestly, and you can see all the way through it — which, around here, is the entire idea.