Hype StackHypeStack

Boilerplates have the wrong default

2 min read, by Hype Stack

All posts
  • architecture
  • saas
  • cli
  • boilerplates

A SaaS boilerplate hands you everything and asks you to delete what you don't need. Why we start empty and add packs instead, and where that trade loses.

Let's start with what a monolithic boilerplate gets right, because it gets one big thing right. You pay, you clone, and inside an hour you have a complete SaaS on your screen. Auth works. Billing works. There is a landing page, an email flow, a dashboard with charts in it. Everything the product can do is visible on day one, and nothing sells like a working demo of your own future app. ShipFast and Makerkit built real businesses on that moment, and the moment is genuine. If your plan is to validate an idea in a weekend and throw the code away either way, that model is hard to beat and we are not going to pretend otherwise.

Our argument is about what happens on day three, when you are not demoing anymore. You are shipping. And now the default that made the demo great starts working against you: everything is in the repo, and everything in the repo is yours to maintain, whether you asked for it or not.

The boilerplate default is subtractive. Here is everything, delete what you don't want. We think that default is backwards, and we built Hype Stack around the opposite one: here is a working nothing, add what you need. The rest of this post is the technical case for that, including the parts where our side of the trade loses.

What deleting someone else's auth actually involves

"Delete what you don't need" sounds like ten minutes with a file tree. Take the most common real case: the boilerplate ships Magic Link auth and you need SSO, or it ships one provider and your company standardized on another. So you delete the auth that came in the box.

Auth is not a folder. In any full-stack codebase, ours included, an auth feature is:

  • middleware that runs on every request and hangs the session on the context
  • a user table, a session table, and foreign keys pointing at them from half the schema
  • route guards wrapping every private page on the frontend
  • a client-side auth context that a dozen components read
  • email templates and the verification and reset flows behind them
  • environment variables, seed scripts, and every test that logs in before doing anything else

We know the shape of this precisely because we ship it. Our smallest starter directory carries 245 files, and auth threads through a large share of them. That's fine when the code arrives as a unit you chose. It is very much not fine when your job is to pull it out of a codebase you opened for the first time on Monday.

The killer is not the volume. It is that you can't tell decoration from dependency. Does the billing code read session.orgId directly or through a helper you're about to delete? Is that onDelete: Cascade on the user relation load-bearing for the teams feature? The person who could answer at a glance is the vendor, and they're not in the room. So you do archaeology: grep for every import, trace every foreign key, run the tests, watch twenty of them fail for reasons two layers away from anything named "auth". Deleting code you don't understand is strictly harder than writing code you do, because writing only requires knowing what you want, while deleting requires knowing everything the original author wanted first.

Most people, rationally, give up and leave it in. (If you are still deciding whether a boilerplate is worth it at all, boilerplate versus building from scratch lays out that call on its own.) Which is how you end up maintaining a stripe webhook handler and three cron jobs for features your product does not have. Every dependency bump, every framework migration, every security advisory now applies to a surface area you never chose.

The template is empty on purpose, and complete anyway

So we inverted the default. The Hype Stack template is a full React 19 frontend on TanStack Router, a Hono backend with Prisma and Kysely over Postgres, Valkey, CI, typed end to end. It builds, runs, and typechecks with zero feature packs installed. It's MIT and free, and "emptied of features" is a design requirement we hold it to, not a marketing line: there is no auth in it, no billing, no notifications, because any feature we bake in becomes the thing the next person has to excavate.

Features arrive through npx @hype-stack/cli compose. The picker walks a fixed order defined in packages/cli/src/core/pack-categories.ts: starter first, then layout, then features. Starter and layout are single-choice, features are a multiselect, and that constraint is doing real work. You cannot install two auth foundations or two app shells, so a whole category of broken combinations is unrepresentable in the picker rather than discovered at runtime. If the project has an admin app, it gets its own layout slot, chosen independently.

When you pick a pack, the CLI copies its source into your repo, shadcn-style. Pick two packs, get two features. The notifications pack lands 74 files across the frontend, backend, admin app, mobile app, and shared enums. That's a lot of files, and every one of them exists because you asked for notifications. There is no file in your repo that got there because some other customer wanted it.

Why this changes the economics, for us and for you

Here's the part that matters beyond any single repo. A monolithic boilerplate has a structural problem: every feature the vendor adds lands in every customer's project. The vendor's growth and the customer's bloat are the same number. Worse, the vendor can never remove anything, because someone is depending on it. The product can only get heavier, forever, and "delete what you don't want" is the customer absorbing that cost on the vendor's behalf.

Composition breaks that link. Our registry currently holds 21 packs: three starters, five layouts, and thirteen features from Stripe billing to a collaborative whiteboard. We can ship pack number twenty, or fifty, and the person who composes a starter plus billing still gets exactly a starter plus billing. The catalog can grow without any individual repo growing. That's also why the free and paid split works the way it does: the template is free and complete on its own, and the paid packs fund maintaining it. We'd rather sell features people opt into than a bundle people dig out of.

Designing for delete

Composition only works if packs behave when their neighbors are missing, so we design every pack for removal from day one. Three concrete mechanisms:

Dependencies are declared and enforced, never assumed. Every pack's manifest.json names its hard dependencies and its optional peerPacks (packs that add behavior if present, nothing if absent). The resolver in packages/cli/src/core/resolve.ts topologically orders the install and throws on a missing or circular dependency. A pack cannot quietly reach into a neighbor the manifest never mentioned, because installs are tested against the manifest, and if you later delete a pack, the things that needed it are named in a JSON file instead of buried in an import three folders deep.

Extension points are dumb on purpose. When the billing pack needs a tab on the settings page, it does not patch the settings component with clever conditional rendering. The starter ships a plain array in settings-tabs.constants.tsx, and a codemod appends an entry to it at install time. Same pattern for nav items, onboarding steps, and admin dashboard rows. Delete the billing pack later and you remove one import and one array entry from a file whose entire job is being a list. No shared abstraction exists to half-break.

Absence fails loudly. Because packs are source code in your repo, not runtime plugins, a missing neighbor is a typecheck failure with a filename, not a feature that silently stops firing. We consider a red build the correct outcome of a deletion. It is the codebase handing you the complete list of what depended on the thing you removed, which is exactly the list the boilerplate archaeologist spends a week reconstructing by hand.

Where our trade loses, honestly

Composition is not free, and we would rather you hear the bill from us.

More assembly. There is no single moment where the whole product runs in front of you. You pick a starter, a layout, packs, and each pack brings its own env vars to fill in. The monolith wins the first hour, decisively. If the first hour is what you're buying, buy the monolith; our comparison of SaaS boilerplates sorts them by what each one is fastest at.

Everything visible is a real feature of the other model. With a monolith you can read the whole app before committing to anything. With a catalog you're trusting descriptions and previews for packs you haven't installed. We ship interactive mocked previews of every pack for exactly this reason, and it's still not the same as the code sitting in your editor.

Codemods are genuinely hard, and they're our load-bearing wall. Writing files is easy. Merging a Prisma schema the user has already edited, or appending to a constants file someone reformatted, is not, and every codemod has to be idempotent because people run compose more than once. This is where our bugs live. One example we've written up internally: our starters carried a config block, marked skip-if-exists, that reached zero generated projects for months, because the target file always existed so the copy never landed and nothing validated it. Nothing broke, which was luck, not design. We now have integrity tests pinning that class of failure, but the lesson stands: the subtractive model pushes complexity onto your delete, and the compositional model pushes it onto our merge. We think a vendor with tests is the right place for that complexity to live. You may reasonably disagree.

Recompose is a promise we have to keep forever. stack.json records what the CLI installed so that adding a pack six months later is safe. That's a compatibility contract between every pack version and every project state, and it only gets harder as the catalog grows. The monolith vendor has no equivalent burden; they shipped you a snapshot and walked away, which is honestly simpler for them.

The default a tool picks tells you whose time it values. Everything-included values the demo. Start-empty values year two, when the repo is yours either way and the only question is whether you understand what's in it. We picked year two. If your project won't have one, pick the demo, sincerely. But if it will, don't accept two hundred files of someone else's decisions as the price of skipping setup. Start empty. Add the one feature you need. Then the next one.

Keep reading.

Hype Stack 1.9: bookings, AI chat, and a free tier you can ship onHype Stack is liveThe best way to start an app in 2026