nSelf started with a billing page. I had Postgres, auth, file storage, and functions spread across three managed providers, and the bill kept climbing without my usage changing. Roughly $200 a month for something that fits on a $5 VPS.
So I wrote a deploy script. About 200 lines of bash. It’s now a Go CLI with 248 command files, and almost none of that growth was the part I expected.
The infrastructure was never the hard part
Docker Compose orchestrates Postgres, Hasura, an auth service, MinIO, Redis, and Nginx perfectly well. That part took a weekend and has barely changed.
What consumed the next two years was everything that happens after it works once. A managed platform’s real product isn’t the database. It’s that somebody else is on call for it. If you’re going to tell people to self-host, you’re asking them to take that on, and the tool has to give most of it back.
The state machine is the actual product
The single most valuable design decision was refusing to have a broken middle state.
Early on, up did what you’d expect: start containers in order, hope they come up. When something failed halfway, you’d be left with a half-started stack, and the recovery path was “figure out which parts are running and fix it manually.” That’s exactly the moment where someone gives up and goes back to a managed platform.
Now every transition is gated on health, and a failed transition rolls back to the last known-good state instead of stopping where it broke. A user should never be looking at a stack that is neither up nor down. They should be looking at a stack that’s up, or one that’s down with an error explaining why.
That’s not a container problem. It’s ordinary distributed-systems discipline applied to somebody’s laptop.
Backups that are actually tested
nself backup drill is the command I’d point at if someone asked what separates this from a compose file.
An untested backup is a hypothesis. The drill takes a real backup, restores it into a throwaway environment, verifies the schema and row counts survived, and tears it down. It’s the difference between “backups are configured” and “backups work,” and those get conflated constantly, usually right up until they matter.
Most backup tooling stops at “the file was written.” The interesting failures are all downstream of that: a dump that restores into a schema mismatch, a restore that succeeds with silent data loss, encryption keys that were never actually exercised. You only find those by running the restore, so the tool runs the restore.
Where it’s genuinely not the answer
nSelf is not competing with AWS for anyone running at scale. The sweet spot is a project doing under 100k requests a day on a $10-20 server, where the entire managed-platform bill is buying you convenience you can automate once.
Above that, the calculus flips and you should pay someone. Below it, you’re renting your own data.
The transferable part
What surprised me is how little of this was infrastructure knowledge and how much was API design. 248 commands is a surface area problem. Every flag is a promise. Every default is a decision you’re making on behalf of someone who hasn’t read the docs and won’t.
The tool got good when I started treating the CLI as a product with users rather than a script with arguments. That’s the same shift that separates a library you wrote for yourself from one other people can adopt.
That transferred in two directions. Outward into the plugin boundary, where the surface stops being mine and starts being an API strangers build against. And sideways into Cascade, which is structurally the same bet made again: a persistent process that owns state, wrapped in an interface designed for someone who hasn’t read the documentation. The domain changed. The problem didn’t.