When I first described Reepolee to people, the question I heard most often was some version of: why isn't this a library? You could publish it to npm, users install it, they get updates automatically, everyone's happy. It's the familiar model. It's how most tools in this space work. Why do something different?
Before the answer, a question worth sitting with: why do you update a library in the first place? Sometimes there's a real reason - a security fix, a behavior you actually need, a bug that has been biting you. But a lot of the time the reason is vaguer than that. The version is outdated. npm audit is complaining. A colleague said you should stay current. The new version adds features you haven't asked for and won't use. The honest answer, more often than people admit, is that the update is driven by anxiety about being behind - not by a concrete need. And yet the update carries real risk: changed behavior, broken assumptions, an afternoon spent tracing a regression to a changelog entry you didn't read carefully enough. A tool that requires you to pull a new version deliberately, and review what changed before accepting it, naturally filters out the anxious updates. You only update when you have a reason.
The answer to the original question has several parts, and none of them are about aesthetics or philosophy for its own sake. They are practical, and they come from watching libraries fail in ways that generators don't.
The Surface of Change
A library has a surface that is hidden from you by design, so when a new version ships the diff is inside someone else's repository, you see a version number change in your package.json, you run the update, you hope nothing broke, and if something did break you find out at runtime, or in a failing test, or from a user who found it first.
The generator pattern is different. When I update the Reepolee generator and you pull the new version, you run it against your project. It produces changes. Those changes are visible - in your working directory, in your diff, in your version control before anything is committed. You read them. You decide whether they make sense in your context. You take what you need and leave what you don't - if the generator updated the route handler but you've already customized yours in a way that works better for your case, you skip that file. You commit what you're satisfied with.
The surface of change is entirely visible because the change is your code now, not a library call or a version number, but actual lines in files you own and can read.
This might sound slower, and in one narrow sense it is. Accepting a library update is one command. Reviewing generated code takes a few minutes. But the narrow sense is wrong about what "slower" costs you. A library update that silently changes behavior costs you a debugging session at the worst possible time. A generated change you reviewed and understood costs you nothing after you commit it, because you know what it does and why it's there.
There's a smaller version of the same benefit in code review. When a library updates in a pull request, a reviewer sees a version bump - one line changed in package.json. When generated code changes, the reviewer sees actual lines of TypeScript. They can read the diff, understand what changed, and raise a question if something looks off. That conversation - "this changes how we handle null in the request parser, is that intentional?" - doesn't happen with a version number. The review is real because the change is visible.
Proximity
There is a concept in architecture that I keep returning to when I think about codebases: proximity. Code that is close to where it runs is easier to reason about, easier to debug, and easier to trust. Code that is far away - behind an abstraction, behind a package boundary, behind a network call - is code you are reasoning about indirectly, through its interface rather than its implementation.
Libraries are architecturally distant because the function you call is not in your repository, its behavior is not something you can read without leaving your codebase, and when it does something unexpected the first step in understanding why is always to go somewhere else, to the library's repository, its changelog, its issue tracker.
Generated code is architecturally close because the template rendering function is right there in your project, in a file you can open, and if it does something unexpected you can put a breakpoint in it. You can read the implementation. You can change it if your requirements diverge from the assumptions the generator made. You own it in a way that has real meaning: not just licensing, but understanding.
I've worked in codebases where a critical piece of behavior lived deep inside a library that nobody on the team had ever read. When it broke, it broke strangely - because the failure mode was in code nobody had mental models of. The fix required someone to read the library source, understand its internal state management, and work around it from the outside. That is a tax. A recurring tax. And it compounds as the library grows and the behavior you depend on moves further from your reach.
This proximity also shows up in stack traces, so that when something breaks at runtime, in staging, in production, at midnight, the stack trace points to your files. Every frame is something you wrote, in a location you recognize, with context you have. With library code, the stack descends into node_modules, into function names that mean nothing until you've read the library's internals. Generated code keeps the entire call stack legible. You debug what you own.
Security Is Local
The supply chain problem in JavaScript is not new, and it is not going away. When you install a library, you are installing its dependencies, and their dependencies, and so on. The code that ends up running in your application is far larger than what you see in your own package.json. Most of it was written by people you have never heard of, maintained by people you have never talked to, and it runs with full access to your file system, your environment variables, and your network. The post on zero-dependency builds goes into the specifics of how this plays out in practice - this section focuses on what the generator pattern changes about that picture.
Generators produce code that lives in your repository, and once generated that code is yours, with no runtime dependency on the generator, no phoning home, no self-updating, no tree of transitive dependencies pulled in when your server starts, because what is in your repository is what runs and nothing else, which is a meaningful difference and not a theoretical one. Auditing a generated codebase means reading your own files. Auditing a library's transitive dependencies means hoping that someone else's npm audit caught everything. The practical difference in how much you actually know about what is running in production is large.
The code you can read is the code you can trust.
When the authentication handler is a file in your project, you have read it. When it's a library call, you have read its API documentation - which is not the same thing.
This matters even more when a formal security audit is involved. Outside agencies scope their work against the repository. With a deep node_modules tree, the scope question becomes uncomfortable: the transitive dependencies are technically the largest part of what runs in production, but auditing them thoroughly at any reasonable cost is not realistic. Agencies typically exclude them, which means the biggest attack surface gets the least scrutiny. With vendored files, the scope is unambiguous - here are the three external files the application uses, they are committed, they have not changed since they were reviewed. An auditor can read them in an afternoon. And when the audit produces a finding in the generated application code, you can fix it directly, in your own file, and close the finding cleanly. A finding in a library dependency sends you to an issue tracker and a waiting game.
Trusted Code Changes
There is a specific failure mode that generators are immune to and libraries are not: the trusted-code update.
This is when a library you depend on, one you have used for years, one with a good reputation and a careful maintainer - gets updated. Not compromised, not abandoned. Just updated. The new version changes a behavior that you relied on. Maybe the change is correct by the library's own contracts. Maybe it fixes a bug that your code happened to depend on. Maybe it adds a feature that has a side effect on the path your application uses. You upgrade because you should stay current, and something breaks in a way that takes time to trace back to the update.
This is not a horror story but routine software maintenance, and it is expensive precisely because it is routine. The cost is not one incident - it is the background tax on every upgrade cycle, the defensive testing you have to do before accepting a new version, the caution that makes people stay on old versions too long, which then creates its own problems.
Generated code doesn't update unless you regenerate and review, which means it doesn't change in ways you haven't seen. The behavior you have today is the behavior you have tomorrow, and if you want different behavior you make a deliberate decision to change it. No automatic side effects from someone else's release schedule.
What the Generator Actually Produces
When Reepolee's generator runs, it produces files - route handlers, template utilities, a server entry point, database helpers, configuration - and these are not magic but ordinary, readable, formatted Bun-native TypeScript that a developer new to the project can open and understand without first understanding the generator that produced it. A developer new to the project can open any file and understand what it does without first understanding the generator that produced it.
The generator is a tool for starting and updating - not a runtime dependency. Once the code exists in your project, the generator could disappear tomorrow and your application would continue to work. That's the relationship we want between a scaffolding tool and the application it creates.
It also means that when your requirements diverge from what the generator assumes - because they will, eventually - you modify your files directly. You don't fork the library or wait for a configuration option to be exposed or open an issue that may or may not be addressed. You change the code. It's yours.
The same applies when you keep your project in sync with a newer version of the Reepolee generator. You pull the update, run it, review the diff, and take what makes sense for your project. If a new version improves the authentication flow but you've already rewritten yours, you skip that part. If the updated database helper is an improvement you want, you take it. There is no all-or-nothing upgrade, no compatibility matrix to navigate. You apply what you need, leave what you don't, and your working application stays exactly that - working.
Vendor Updates Work the Same Way
The same logic applies to the third-party libraries that do appear in an Reepolee project. These are vendored: a single minified ESM file, downloaded once with an explicit version number in the command, committed to the repository. Updating Zod means changing zod@4.4.3 to zod@4.5.0 in the get:zod script, running it, and getting a new file. That file lands in your working directory as a diff. You can inspect it, test it in isolation, and only commit it to the project once you're confident nothing changed in a way that matters to you.
The discipline this creates is the same as with generated code: you update when you have a reason, not because a tool told you the version is outdated. If the app works, the tests pass, and there's no security advisory - there's a good chance you don't need the update at all. A working application with a slightly older vendored library is a better outcome than a broken application with the latest one. The version number in your package.json is not a measure of code quality. It's just a number.
Try the Upgrade Anyway - Just Don't Ship It
The discipline of updating only when you have a reason is sound, but it has a blind spot: the longer you hold a version, the less you know about what upgrading it would cost. A security advisory doesn't care about your maintenance schedule, and neither does the compatibility break that lands in version X+3 while you were content on version X. When those moments arrive, the last thing you want is to be discovering the migration cost under pressure.
What we do instead, every few months on the libraries and tools that matter to the project, is to run the upgrade locally with no intention of shipping it. Pull the new version, run the tests, read the diff, and look at what changed - not to adopt the change, but to understand the distance between where you are and where you would need to be. If the upgrade is clean, you file that away and move on, knowing that when you do need it the path is clear. If the upgrade reveals something significant - a changed API, a broken test, a pattern the new version handles differently - you know that too, and you know it now, before you are in a situation where you have no choice but to fix it immediately.
The upgrade gets discarded. The knowledge doesn't. You close the branch, delete the stash, and the application stays exactly as it was - except that you now have a mental model of the upgrade surface that you didn't have before. When the real reason eventually arrives, and it will, you are not discovering the work for the first time. You are executing a plan you already made.
This is not a formal process and doesn't need to be. An hour every quarter, a local branch that never gets pushed, and the habit of actually reading what changed rather than just checking whether the tests still pass. The libraries we vendor are few enough that this stays cheap. And the cost of not doing it - of treating each version as frozen until forced otherwise - is that you eventually pay for all that accumulated distance at once, in an environment where you can least afford to.
Deploy Without a Registry
There is a practical consequence of zero runtime dependencies that doesn't get discussed much because it sounds boring: your deployment cannot fail because of someone else's infrastructure.
When a project requires npm install at deploy time, it requires npm to be reachable. It requires every package in the tree to still exist at the version specified in the lockfile. It requires the network between your server and the registry to be functioning. These things are usually true, and then occasionally they aren't - at exactly the wrong time. A package gets unpublished. The registry has an outage. A corporate firewall blocks outbound connections to external package registries. npm install hangs or fails, and your deploy stops.
A project that deploys with git pull and a process restart has none of these failure modes. The repository is the complete truth of what runs. If the repository is accessible, the deploy works. This is not a small thing for the kind of team this is aimed at - as covered in more detail in the post on zero-dependency builds, the simpler the deploy, the smaller the blast radius when something goes wrong at an inconvenient hour.
It also makes Reepolee usable in environments where outbound connections are restricted or prohibited entirely. Schools, hospitals, government agencies, and enterprise environments frequently operate networks where a server cannot reach external package registries at deploy time - either by policy, by firewall, or by design. For these contexts, a framework that requires npm install to function is simply not deployable without additional infrastructure work. A framework that needs only git and a running Bun binary deploys cleanly in the same environment, with no special configuration. If you're running on your own VPS or on-premise, this matters more than it might seem.
The Honest Trade-off
None of this is free. A library that updates automatically means you get bug fixes and security patches without any effort on your end. A generator means you have to pull updates deliberately and review what changed. That is real work, and it is worth naming honestly rather than pretending the generator pattern is strictly better in every dimension.
The trade-off is: automatic updates versus visible change. Implicit behavior versus owned code. Ecosystem velocity versus local understanding.
For the kind of project Reepolee is built for - small to mid-sized applications, small teams, long maintenance horizons, deployments that need to be understood by whoever is on call - local understanding wins. An application that one person can fully reason about, debug without leaving the repository, and trust because they have read it, is more valuable than one that is always current but always partially opaque.
The generator pattern is a choice about where trust should live. Not in the registry. Not in someone else's release schedule. In your own repository, in files you have read, in code you own in a way that means something.