$ ship-small_
a3f27b1 · 2026-07-15 · 7 min

Picking a boring stack (again) for the fifth time

For my last five side projects I have started by considering exciting stacks. For the last four, I have ended up with the same boring one, and shipped weeks faster.

I have a small, embarrassing ritual at the start of every side project. I spend about half a day looking into whichever new framework the front-page discourse has decided is worth caring about, convince myself I should adopt it, spend another half-day setting it up, hit some genuinely dumb papercut, close the tab, and go back to my boring stack. This has happened four times in a row now. I want to write down the boring stack, in case anyone else is enacting the same slow-motion crime against their own weekends.

The stack

# backend
python 3.12
fastapi
sqlite (yes, sqlite) with WAL
apscheduler for periodic jobs
uvicorn + gunicorn behind nginx

# frontend
plain html + a very small amount of htmx
one hand-written css file (~500 lines)
zero npm dependencies

# infra
a single $12/mo VPS
caddy or nginx for TLS
systemd for the app process
litestream for sqlite → S3 backup
uptime-kuma on a second $5/mo box

# ops
one bash file: deploy.sh
one bash file: restore.sh
one env file: .env
that's it.

None of the above is fashionable. Almost all of it has been the answer to some form of "should I pick X instead" for at least five years. That is, I think, its main virtue.

Why sqlite instead of postgres

Because for anything I build alone, sqlite is a database in the same way an axe is a tool. It is one file. It has no separate process. It has no separate credentials. It has no separate backup story once I add litestream. It has an order of magnitude more predictable performance for a single-writer workload than a lightly-tuned postgres deployed by someone (me) who is not really an ops engineer.

When and only when I add a second writer — a background worker on a different machine, a horizontally-scaled API — I move to postgres. I have done this exactly once. It took an afternoon. All the previous "but what if you need postgres someday" tabs I closed were, in retrospect, correct to close.

Why htmx instead of react/svelte/whatever

Because the number of pages on any of my side projects that need genuinely interactive UI is small. Most of the "interactivity" is: click a button, replace a small piece of the page. htmx does that in one attribute. My CSS is one file. My deploys have no build step. When I need real interactivity (a canvas, a live chart), I add it as a small vanilla-JS island on that one page. I have never wished I had a component framework at 2 a.m. I have often wished I had fewer moving parts at 2 a.m.

Every dependency you skip is a dependency you never have to upgrade.

Why fastapi and not "something more modern"

Because I know it. I can type its imports in my sleep. I can predict its error messages. I can hire a bootcamp graduate who can maintain it. I can pattern-match a bug in it to one of about six categories inside twenty seconds. That is what a mature tool feels like from the user's side, and it is worth more than every clever ergonomic improvement a newer framework offers, at my scale of project. I will happily reconsider when I have twelve teammates. I don't.

Why a single $12 VPS

Because for the first thousand users of anything, one boring VPS is enough, and the operational complexity of "serverless" (cold starts, obscure timeouts, opaque billing, request-scoped connection pools, split logs) exceeds the operational complexity of one machine that I ssh into like a normal person. When traffic actually requires a second machine, I add one. Nobody in my history of side projects has yet made me add one.

What I do not skimp on

Three things I always spend the "excitement budget" on, even when the rest of the stack is boring:

What "boring" is actually paying for

Every hour I don't spend on stack choices is an hour I spend on the actual thing the user pays for. That is, in the end, the only defensible reason to like any tool. The five projects I have shipped on this stack all reached first-paying-customer inside three weeks of the first commit. The one project I shipped on a fashionable stack took six weeks to reach first-paying-customer, and I spent one of those weeks tracking down a build-step incompatibility that had precisely zero to do with the problem the customer was paying me to solve.

Pick this fight later

You are allowed to try the new hotness. Just do it on a project where the goal is to learn the stack, not to ship the product.

Get the next log entry

One long-form devlog post most weeks, in your inbox. Plain text, no tracking, unsubscribe with one click. Never any launches.

free · plain text · no read tracking