BlogZero dependencies on purpose
Why almost everything I ship is one self-contained file, and the sentence I write at build time that keeps that from turning into a mistake.
I have a dozen live things running right now. Games, marketplaces, directories, a couple of tools I open every week. One person, no ops team, no on-call rotation. The way that stays possible is boring and deliberate. Almost everything I ship is a single self-contained file, or a stack with no external services underneath it. No managed database. No third-party account in the critical path. No monthly bill I forgot I was paying.
That sounds like the constraint you accept when you cannot afford better. For me it is the reason the lights stay on.
The default recipe
When a new app idea passes the smell test and I want to see if it is real, I reach for the same stack every time. Next.js for the app, node:sqlite for the data, Tailwind for the look. That is the whole list.
node:sqlite is the quiet hero. It ships inside Node itself as of 22.5, it is pure JavaScript, and it needs no native build step. If you have ever tried to compile a native SQLite binding on Windows, you know why that last part matters. No toolchain to install, no gyp errors at 11pm, no "works on my machine, breaks on the server." It just runs.
I keep all the SQL in one module. Schema, seed data, and every query live together in lib/db.ts, and nothing else in the app talks to the database directly. That one rule pays for itself later. I will come back to why.
Deploy is almost aggressively unremarkable. I build in standalone mode, tar the output, scp it to one small server, and point a systemd unit at it. No container registry, no orchestration, no platform dashboard. The first thing I shipped this way was a coach marketplace. A city's worth of bookings is trivial load for SQLite. People hear "SQLite" and picture a toy. It is production-grade at single-server scale, and single-server scale covers more real businesses than anyone likes to admit.
The games go further
Every game I have built is a single vanilla JavaScript file driving a canvas. No framework, no build, no npm install at all. You open the file, it runs. Five years from now it will still run, because there is nothing underneath it that can rot. No package that gets abandoned, no peer-dependency war, no lockfile that resolves differently on a Tuesday. The file is the whole thing.
This has a real cost. I write more by hand than a framework user does. In return, the number of things that can break on their own, while I am asleep and touching nothing, sits close to zero.
The QR generator, and why I vendored the encoder
One small decision captures the whole philosophy. I built a QR code generator. The lazy version calls a QR API. You send it a string, it sends back an image. Ten minutes of work.
I did not do that. I took an MIT-licensed QR encoder and pasted it straight into the file. I own the encoding now.
The reason is specific. A QR code usually ends up printed on something. A poster, a flyer, a sticker on a table. Once it is printed, it is frozen. Lean on an external API and two things can go wrong later, both of them the kind you discover at the worst possible moment. The API can rate-limit you on the day traffic spikes. Worse, the service can change how it renders, or vanish, and a code somebody already printed and hung on a wall stops resolving. I cannot go recall the posters. A printed QR code has to work forever, and owning the encoder is the only version of "forever" I actually control.
Write down when temporary ends
Here is where the usual "just use SQLite, keep it simple" advice quietly fails. The danger with temporary infrastructure is not that it is temporary. It is that nobody ever defines when temporary is supposed to end. The cheap start has no expiry date, so it becomes the permanent architecture by default, and one morning it is holding real money in a single file on one box.
So I write the exit conditions at build time, before I am attached to anything. The SQLite data layer graduates to managed Postgres the moment any one of these fires:
- Real money starts flowing. Live payments mean I need point-in-time backups and recovery, not a database file on one server hoping the disk holds.
- I need more than one server. SQLite is a file on a machine. It cannot be shared across machines, so the second I want two, the file has to go.
- Concurrent writes show up. Multiple staff or systems writing at once is not what SQLite at this scale is the right tool for.
- Someone other than my own tooling needs to query the data. A dashboard, my wife's laptop, anything wanting direct access. That is the signal it needs to live somewhere built to be queried from the outside.
Until one of those fires, I mitigate instead of migrate. A nightly cron copies the database file off the box, so the worst case is losing a day, not everything. I also pin the Node major version, because node:sqlite is still marked experimental and the real risk is the API drifting between majors. If it ever does, the escape hatch is already drawn. Swap in better-sqlite3, which has a near-identical synchronous API, inside that one lib/db.ts module. This is the moment the single-module rule earns its keep. The blast radius of the swap is one file.
When a trigger does fire, I schedule the Postgres move as its own real task. Roughly a day of work to rewrite the module and copy the data across. I do not limp past the line pretending it is fine.
The actual point
A dependency is complexity you borrowed and do not control. Every external service is an account someone has to manage, a bill that recurs, a rate limit you will hit eventually, a behavior that can change under you without asking, a login that breaks at 2am while you sleep. You did not write that code, and you cannot fix it when it fails. You can only wait for the vendor.
Owning the stack is slower to start. It is much cheaper to live with, and living with it is where almost all of the time goes.
The whole thing reduces to one line worth stealing. Cheap-to-start is only safe if you also write down, in advance, the exact moment cheap-to-start stops being the right call. Write that sentence at build time, while you are not yet attached to anything, and the simple version stays a good decision instead of quietly becoming a problem you inherited from yourself.