Three stacks

July 25, 2026

People ask what my stack is, and I have never had a good one-line answer. The three things I have built most recently share almost no infrastructure. One is a Go binary, one is a Next.js app spread across four vendors, one is a browser app with a server that is barely there.

For a while I thought this meant I was inconsistent. I have come around to thinking it is the point. In each case I picked one property I refused to give up, and the stack is mostly a consequence of that choice rather than a preference I brought with me.

The one that cannot use the network

The rule was that the binary may not make a network call. Not for a model download, not on first run. That single sentence removed most of the options.

No network callsand one binary
Go, CGO_ENABLED=0cross-compiles to 5 platforms from one machine
SQLite compiled to WASMa C build would forfeit that
PDFium under the same runtimesame reason
The model, written by handno Python, no ONNX runtime
Every choice on the right is forced by the sentence on the left.

Anything with C in it forfeits static cross-compilation, so both SQLite and the PDF engine run as WebAssembly inside the process. The embedding model had to be compiled in, which meant it had to be small, which meant quantizing it and reimplementing its forward pass in plain Go.

None of that is a stack I would have chosen from a menu. It is what was left after the constraint.

The one that cannot touch the bytes

The photo product had a different rule: the application server never sees file data. Uploads go from the browser straight to object storage, and the server only ever hands out permission.

That pushes almost everything to the edge and leaves the app doing coordination. Storage is Cloudflare R2. Image resizing, watermarking and ZIP streaming happen in a Worker next to the bucket. The heavier work that cannot run in a Worker runs in a container. The Next.js app on Vercel is left holding the database, the auth, and the job queue, which is a much smaller job than it sounds.

This is the only one of the three with a genuinely large stack, and I am not entirely comfortable with it. Five vendors is five status pages.

The one that cannot read its own data

The vault's rule is that the server must never be able to read a secret. All the cryptography therefore has to happen in the browser, which inverts the usual weight of an application.

The client is a Vite and React single-page app doing key derivation and encryption. The server is Hono on a Worker with SQLite underneath, and it does almost nothing: check a session, store an opaque blob, hand it back. When the server is not allowed to understand the data, there is very little left for it to do, and the stack gets smaller in a way that is quite pleasant.

What is actually constant

Underneath three unrelated architectures, the same handful of things show up every time.

LayerHaypileLensdropKlef
LanguageGoTypeScriptTypeScript
UIPreactReact 19React 19
StylingTailwind 4Tailwind 4Tailwind 4
DataSQLite, embeddedPostgresSQLite on D1
Validationhand-rolledZodZod
EdgeCloudflareCloudflareCloudflare
PackagesGo modulespnpmpnpm

The pattern I notice reading that back is that I am conservative about the boring layers and adventurous exactly once per project. Tailwind, Zod, pnpm and Cloudflare are not decisions any more, they are defaults, and that is deliberate. Spending novelty budget on styling means not having it available for the part that is actually hard.

The other constant is a preference for SQLite-shaped storage. Two of the three run on it, and the third only uses Postgres because it genuinely needs concurrent writers and row-level security.

Counting the cost

The clearest difference between the three is not the architecture, it is how much I am depending on other people.

Haypile (Go)17 depsThis site21 depsKlef45 depsLensdrop82 deps
Direct dependencies only. Transitive trees make the gap considerably wider.

Seventeen direct dependencies is not a virtue on its own. It is a consequence of Go having a usable standard library and of the no-network rule ruling out anything heavy. The photo product's eighty-two is not a failure either; it does more, for more people, with payments and image processing and background work.

But the gap is worth looking at honestly, because every one of those is a package that can be abandoned, compromised, or break on a major version. That is the actual cost, and it is paid later rather than now.

What I stopped reaching for

A few things fell out of my defaults, and the reasons are all the same shape.

I stopped adding state management libraries. Between server components and a small amount of local state, I have not missed them. I stopped writing my own auth, because the failure mode is silent and severe and the hosted options got good. And I stopped picking ORMs that hide the query, because the times I have been burned were all cases where I could not see what was being run.

Reflections

The advice I would actually give is not to have a stack. It is to be able to say, for each thing you build, which property you are unwilling to trade. Once that is written down, most of the choices stop being preferences and start being consequences, and the resulting arguments are much shorter.

The failure mode is picking the constraint after the tools. I have done that, and what you get is a rule that conveniently never rules anything out.

The site you are reading this on is the fourth answer, and its constraint was that it should be almost entirely static text.