Boring on purpose
The stack I picked for FeedFilters is deliberately boring. A single Go binary, embedded SQLite, server-rendered HTML, vanilla CSS and JS, packed into a Docker image the size of the binary itself. One process to run, one file to back up, no Redis, no Postgres, no React, no message broker, no build step. For a solo project, that shape matters to me more than almost anything else I could optimize for.
Operationally, I want as little surface area as possible. Every moving part is something I’d have to think about when it breaks at 11pm, and I don’t have a team to share that with. The fewer pieces now, the fewer pages later.
Go appealed to me on a few axes. It’s fast enough for anything I’m likely to throw at it. The standard library is honestly large enough to build a real app with, which I find rare among modern languages. Concurrency is a first-class part of the language rather than a third-party concern. And the build pipeline is dramatically simpler than what I’m used to — one binary, no runtime to install on the host, no dependency hell. Code written today will, by Go’s explicit promise, still compile in ten years.
There’s a wrinkle I should be honest about: aside from going through the Go tutorial many, many years ago, I haven’t actually written any Go. So picking it for this project is itself part of the experiment — can I lean on Claude to do the Go writing while I drive the shape of the program at a higher level? That’s the bet, and the answer so far has been an emphatic yes, but I’ll have more to say about that once I’ve got more miles on it.
SQLite is the part of the stack I do know. I’ve been using it on and off for years for small projects and side tools, and the pitch hasn’t changed: it’s a file. There’s no separate database process to run, no network hop on every query, and backups are as simple as copying that file somewhere safe. It’s plenty fast for a read-heavy workload like this one, especially with the indexes set up well. And, like Go, it’s something I can reasonably expect to still be working in a decade.
The web side is server-rendered HTML, a single hand-written CSS file,
and a small amount of plain JavaScript. No React, no Vue, no Svelte,
no build step for the frontend at all. Templates render on the server,
the browser gets HTML, and a few lines of vanilla JS handle the bits
that genuinely need it. This is also a deliberate choice. SPA
frameworks are excellent at certain kinds of problems, and FeedFilters
isn’t one of them. Pages submit forms and navigate; there’s no
real-time anything. Skipping the framework saves me a build pipeline,
a node_modules to keep current, a separate frontend deploy story,
and a category of bugs that comes from running the same logic on two
sides of the wire.
Deployment continues the same instinct. The Go binary lives inside a
Docker container built from FROM scratch — the image contains
the binary, a CA bundle for outbound HTTPS, and literally nothing
else. No shell, no package manager, no init system. If the binary
crashes, the container exits, and restart: unless-stopped brings it
back. That’s the entire supervision story. The same image runs
locally, in CI, and in production, with a single docker-compose file
describing the production shape and a single host running it. No
Kubernetes, no orchestration layer, no service mesh. When something
goes wrong I have one place to look.
Every choice in this stack comes back to the same instinct: pick the things I won’t have to keep up with. The point isn’t moving faster while I build it. It’s not having to keep thinking about this service once I’m done working on it.