I sat down to build RSVPs for three grapplers. I did not sit down to write a QR encoder.

Five weeks later there is a 600-line QR encoder inlined into worker.js. There is a PNG generator written in 80 lines of stdlib Python because I needed a 512x512 favicon and refused to install a library. There is a hand-rolled markdown parser, because /articles had to render markdown and I could not bring myself to type npm install marked. There is a multipart form parser, also by hand, because Cloudflare Workers' formData() is broken for binary uploads and the fix is to read the raw ArrayBuffer and walk the bytes yourself. None of this was on the plan. The plan was three grapplers and an RSVP form.

The plan got away from me. It got away from me earlier too.

What did not get away — what I held onto, week over week, commit over commit — is the property that package.json does not exist in this repo. There is no node_modules. There is no package-lock.json. There is no npm install. When you clone this codebase, you do not download four hundred packages from a registry that neither you nor I have ever audited. You download one file from git.

This article is about why. It is also about what that doesn't fix.

The threat model

This is not a hypothetical. The last few years of supply-chain incidents have been a steady, named, dated drumbeat:

The throughline is not "npm is bad." The throughline is transitive trust does not scale. When a project depends on fifty packages, and those packages depend on twenty more, you are not trusting fifty maintainers. You are trusting thousands. Each one is a single bad day, a single compromised laptop, a single sold GitHub handle away from running code inside your build. The math is unkind and getting worse.

What "no dependencies" actually looks like

The instinct, when you hear a project lives without npm, is to imagine Spartan minimalism — the developer sits cross-legged at a stone keyboard, refusing the modern conveniences. In practice it's not that. It's a series of small no's, where each no makes the codebase slightly bigger and the trust surface slightly smaller. This part of the platform spiralled exactly the way the rest of it spiralled: one decision at a time, none of them planned.

Receipts:

None of these are virtuoso engineering. They are small pieces of unexciting code. The cumulative effect is that the entire dependency graph of this platform fits in your head.

The same posture extends to the Python side. The ops toolchain — deploy.py (POSTs worker.js to Cloudflare's REST API), setup.py (one-time KV namespace creation), backup.py (snapshots KV + R2 to a tar.gz), restore.py (the inverse, gated by a "type YES RESTORE to proceed" prompt), migrate_training_to_year.py (a one-shot data migration), generate_favicon.py (the 80-line PNG generator from earlier) — all of it runs in a venv whose pip freeze returns nothing.

Every import in every script comes from the Python standard library: argparse, json, urllib.request, tomllib, subprocess, pathlib, tarfile, hashlib, struct, zlib. The favicon generator is struct plus zlib. The Cloudflare "API client" is urllib.request with a Bearer header on the requests. The config parser is tomllib, which has shipped in Python since 3.11. There is no requests. There is no python-dotenv. There is no cloudflare SDK, no boto3, no pydantic. There is no requirements.txt. There is one external subprocess call, to git rev-parse --short HEAD for cache-busting the worker, with a hardcoded argv list and a timestamp fallback if git isn't on the box.

The argument was never about JavaScript specifically. It was about transitive trust. The same threat model applies in any language with a package manager, and the same answer — "use the standard library; write the rest yourself" — applies in any language with a usable standard library.

What we give up

Honest accounting:

What I do not give up by skipping npm: a frontend that works, a deployment that is reliable, and a codebase I can read end-to-end in an afternoon.

The dependencies we do have

I want to be precise here, because "zero dependencies" as a marketing claim is almost always a lie and I am not going to start.

The platform has four runtime dependencies:

Four named relationships, four contracts I can read. That is qualitatively different from "we import 47 packages which transitively import 800 more, none of whose maintainers I can name." The dependency tree fits on a napkin. When something changes upstream, I know where to look.

This is the actual delta. Not "zero" versus "some." Few-and-named versus many-and-anonymous.

What this doesn't fix

Now the part I owe you.

Removing npm from the equation closes one class of bug: the class where a maintainer I have never met ships code that runs in my build pipeline tomorrow morning. It does not close the class where I myself ship code that has a bug in it.

There are 26,695 lines of JavaScript in worker.js. They were written by one person over thirty-seven days, with a pair-programmer (Claude Code) that is very good at making things compile and only adequate at noticing security implications I haven't already flagged. The first ten thousand lines were written before I'd implemented addSecurityHeaders. The CSP nonce migration happened in week four. The training-key consolidation that cuts heatmap reads by 100× shipped the day before this article was drafted. The codebase has the shape of an artefact that was sprinted into existence and is still being hardened around the edges.

A non-exhaustive list of things I think are reasonably solid:

A non-exhaustive list of things I do not yet know are bugs:

This is the honest part. The platform might have a critical vulnerability you haven't found. I might have a critical vulnerability I haven't found. The right person to find one of these is not me, alone in my apartment, but a stranger on the internet who happens to look. If that stranger is you: there is a .well-known/security.txt on every domain. The contact is info@grapplers.club. Responsible disclosure gets a fast reply and a fix; the alternative does not.

The argument for "zero npm" is not that the platform is therefore secure. The argument is that one large category of risk has been deliberately removed, so the remaining risk fits in a smaller box that one person can reason about. That box is still not empty. It is just small enough to inspect.

Where this lands

The reason this post exists is that the previous one ended on "I should probably have stopped at the WhatsApp group" — which is true, and which is also incomplete. The thing that also happened, in those five weeks, is that I made a series of small architectural decisions whose only justification at the time was "yeah, but no" — yeah I could pull in this package, but no, I think I'd rather not — and the accumulated weight of those small no's is what makes the codebase auditable today.

I did not plan to write a QR encoder. I did not plan to hand-roll a multipart parser. I did not plan, especially, to inline an MIT-licensed PNG generator that I will probably never need to modify again. None of those choices were strategic when I made them. Each one was a hesitation. Each hesitation, in retrospect, was a small loan I declined to take out against future me's threat surface.

Three grapplers. One worker.js file. Zero npm packages. Some unknown number of bugs.

Still better than the WhatsApp group.