Why OTF Kits Stop at 80% — And Why That's a Feature
The kit that knows when to stop
Most "starter kits" ship a kitchen sink. Auth, billing, RBAC, multi-tenancy, CMS, analytics, email templates, i18n, theming, admin panels — the README reads like a SaaS in miniature. That's a feature on the landing page and a tax on your codebase from week two.
The hard part of a kit isn't what it ships. It's what it leaves out.
OTF deliberately ships the 80% that's the same in every product and stops at the seam where your judgment matters more than ours. That's not laziness. That's the product.
The tax of "does everything"
Every opinion baked into a starter is a future commit someone has to undo. A 12-role RBAC system is great until you discover your real access model has 3 roles and 2 special cases that don't fit the hierarchy. A multi-tenant schema with org_id on every table is great until your tenant boundary turns out to be wrong — wrong column, wrong scope, wrong join path. A CMS panel is great until your editors want Notion, not a custom admin.
The kitchen-sink kit optimizes for the first hour of use. It is hostile to the first week, the first quarter, and the rewrite you'll eventually do anyway.
Three failure modes show up consistently:
- Baked-in abstractions you don't need. A
useOrganization()hook that fires on every render. A<RoleGate roles={['admin','owner']}>wrapper around your actual ACL logic that lives in the API. You end up writing through the abstraction instead of around it. - Schema choices that lock you in. Soft-delete columns, audit fields, optimistic-locking version columns. Helpful defaults — until your auditor wants hard deletes with a signed reason, or your product decides versioning shouldn't be per-row.
- Hidden dependency surface. The kit "includes" Stripe, but it's a Stripe SDK pinned to a specific version. The kit "includes" auth, but the session shape only works for one provider. You discover this on a Friday at 5pm.
11 production screens. Login, database, payments — all wired.
The SaaS Dashboard Kit ships everything already connected. Nothing to set up. Live demo at saas.otf-kit.dev.
What's actually in the box
OTF ships the parts that are the same across nearly every product. Concretely:
- ~200 components with the same name, props, and look on web, iOS, and Android.
<Button>is<Button>everywhere — one API, one theme. - Design tokens that flip one variable and the entire app re-skins. Web and mobile stay in lockstep because they read the same source.
- Auth + billing + DB + Stripe wired end-to-end in the SaaS Dashboard kit. Not "ready for you to wire" — actually wired.
- A 24-item design checklist that runs before any kit ships, so the components you import don't drift in quality.
- AI-tool configs: a
CLAUDE.md, a.cursorrules, and 20+ tested prompts underai/prompts/. An agent extends the kit; it doesn't regenerate it. - One production script that wires a custom domain, DNS, TLS, and a mobile build. From
npm run devto ` without leaving the terminal.
These are choices that are wrong to delegate. Buttons behave like buttons. Tokens re-skin cleanly. The deploy script is the deploy script. We make these decisions so you don't have to.
What we deliberately leave out
Here's the list. Not because we forgot — because the wrong answer here is more expensive than no answer.
Multi-tenant org boundaries. OTF ships a single-tenant user model. If you need orgs, teams, workspaces, or accounts-of-accounts, you model them yourself. Every product's org model is a little different (orgs own users, or users own orgs, or both are siblings under a workspace), and getting this wrong at the schema layer is a six-month migration.
Custom animation system. No spring physics, no choreographed transitions, no motion library. The components use platform-native animations where they exist and CSS transitions elsewhere. If you need a physics engine, install one — but don't expect one in a kit that's supposed to ship the boring 80% first.
CMS / page builder. No headless CMS UI. No drag-and-drop blocks. No content models in the admin panel. If your editors live in Markdown and Git, fine. If they live in Notion, fine. Either way, that's a product decision, not a kit decision.
Analytics event taxonomy. OTF doesn't ship a track() function with predefined events. The shape of your funnel is your shape. We give you a typed client if you want one; we don't tell you what to count.
Email templates. No transactional email design system. No MJML wrapper. No "welcome email" component. Your voice, your templates, your ESP.
RBAC beyond a role field. A single role string on the user. If you need a permission matrix with resource scopes, conditionals, and inheritance, model it. We won't pretend three hardcoded roles is an access-control system.

Each of these is a place where "baked in" means "wrong by default" for at least half the people who'd use it.
Why "bake it in" is a hidden failure mode
The argument for shipping everything is always the same: "developers won't build it themselves." Two things wrong with that.
First, the 80% of the kit that is shipped — buttons, forms, navigation, layout, tokens, deploy scripts — that's the part developers don't build themselves. That's the actual use. Stuffing more in past that point doesn't compound the use; it dilutes it.
Second, "developers won't build it themselves" assumes the builders are the same as the customers. They're not. The customer wants their billing flow to feel native to their product. The builder wants the auth boilerplate to stop eating their weekend. Solving the second problem doesn't require solving the first.
The seam between "shipped" and "left to the builder" is the seam between infrastructure and product. Kits that confuse the two end up shipping product-shaped infrastructure that fights your actual product the moment it tries to be itself.
The seams are real
Leaving things out only works if the boundaries are clear. Otherwise it's just "missing features with good marketing."
The seams show up in three places:
1. The CLI respects your repo, not ours. Two install paths, your call.
# Copy-paste: you own every line, no version drift.
npx @otfdashkit/cli add dashboard
# npm install: versioned package, you upgrade on your schedule.
npm install @otfdashkit/dashboardEither way, the kit doesn't reach into your codebase to wire itself up. There's no init step that mutates your package.json and walks away.
2. Components are props-in / events-out. No hidden global state, no context providers you can't see, no HOCs that wrap your render tree. You can read a component file and understand what it does. That makes "extend it yourself" actually possible instead of theoretically possible.
// A component you can read in 30 seconds
export function Button({ variant = 'primary', size = 'md', children, ...rest }) {
return (
<button className={cn(buttonStyles({ variant, size }))} {...rest}>
{children}
</button>
)
}No <RoleGate>. No <AnalyticsProvider>. No <TenantBoundary>. You add those if you need them — and you add them to your code, not to the kit's.
3. The AI configs know what they don't know. The CLAUDE.md lists the conventions. The prompts assume them. But they don't pretend to know your data model, your access control, or your analytics shape. An agent working in a kit that respects these seams extends it correctly; an agent working in a kitchen sink invents rules to paper over the seams it can't see.
What this gets us
A kit that's the same shape on day one and day 365. No rewrites because the access model changed. No migrations because the org boundary moved. No Friday afternoon because the email template hardcoded the wrong logo.
When the model under your code changes — and it will — the durable layer stays. The components still render. The tokens still theme. The deploy script still ships. The product layer above it is yours to evolve, unfettered by opinions you didn't pick.
Kits that try to be your product usually end up fighting your product. Kits that know where to stop are the ones you keep.
Ship the product, not the setup.
- 11 production screens — auth, billing, team, analytics, settings
- Real database, payments, and login — all wired on day 1
- AI configs pre-tuned so your agent extends instead of regenerates