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:
- The domain, and a decent DNS provider. Cloudflare or hurricane electric — not the registrar's DNS.
- Backup. Litestream to S3 for sqlite. A weekly restore rehearsal to a fresh VPS. If I've never restored, I don't have a backup; I have a hope.
- Monitoring. Uptime-Kuma on a separate cheap box that pings the app every minute and my phone if it goes down for three consecutive minutes. This has saved me twice this year.
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.