Solo shipping is choosing your debt deliberately
Shipping fast isn't picking speed over quality. It's picking which debts to take on, which to refuse, and naming the cost upfront so the bill doesn't surprise you later.
Apr 29, 2026
The version of this story you usually hear is "move fast and break things." It's not a useful version. Founders and solo engineers don't actually break things on purpose — what they do is take on debt, sometimes deliberately and sometimes without noticing.
Shipping a multi-tenant matrimony SaaS solo, in two months, alongside a day job, taught me that the difference between sustainable speed and a slow-motion crash is not the volume of debt you take on. It's whether each shortcut has a named owner, a named cost, and a named trigger for when you pay it back.
Here are four calls I made on that build, and what it cost me to make them.
Schema-per-tenant Postgres: paid for the right thing
The default multi-tenant pattern is shared-schema-with-a-tenantId-column. Every
table has tenant_id, every query filters on it, and a single bug in your
ORM's where-clause leaks one tenant's data to another.
For a matrimony platform — where leaking a profile across tenants would
end the product's credibility in a single Twitter thread — that pattern was
the wrong shape of risk. So I went the harder way: each tenant gets its own
PostgreSQL schema, and the connection routes through search_path = tenant_{slug},public. Tenant tables don't have a tenant_id column at all.
The schema is the isolation.
The cost is real. Migrations run per-schema, not once across the cluster.
Provisioning a new tenant means cloning a template_tenant schema. The first
time a migration silently failed on one tenant, I noticed it from a 500 error
in the logs, not from the deploy.
I'd take this debt again. The thing I was buying — no SQL path that lets one tenant read another's data, by construction — is exactly worth the operational tax for this product. Schema-per-tenant is appropriate debt: I took it on because I needed the property it bought. I named the cost (per-schema migration overhead, observability tax) and budgeted for it from day one.
Two Next.js apps instead of one: overpaid
I split the codebase into a public app (port 3007) and an admin app (port 3008) on day one. The reasoning was deploy isolation: admin should be able to ship features without redeploying the customer-facing site, and vice versa.
That reasoning was correct in the abstract and wrong in the specifics. ~80%
of the dependencies overlap. Auth wiring runs in both. Two package.jsons,
two pnpm installs, two Vercel projects. The deploy-isolation payoff would
matter at five engineers; at one, it just means I redeploy two things instead
of one when I touch shared code.
I didn't see this cost on day one because day-one me wasn't the person paying it. Day-sixty me was. That's the failure mode of unbudgeted debt — the person making the decision isn't the person who lives with it.
If I shipped this again, it'd be one Next.js app with role-based routing and a separate admin entrypoint. I'd give up the deploy isolation and accept the larger blast radius, because at solo scale the operational cost dominates the isolation benefit.
The horoscope engine: build, don't buy
The kundali matching engine is the actual differentiator of the platform. It takes two birth charts, computes horoscope compatibility (guna milan, mangal dosha, nakshatra alignment), and returns a categorised match score.
The "easy" choice was a paid horoscope API. There are several. Pricing is
per-call, somewhere in the cents, and the integration is a fetch away.
I built it instead, on top of astronomy-engine — an MIT-licensed library
that does the actual astronomical math, no API calls, runs locally. The
domain rules (which planet means what, how the score combines) are mine.
The build-vs-buy framing on this is straightforward when you do the math. A matrimony platform runs compatibility checks aggressively — every Smart Match query, every profile-pair view, every pre-filter before a connection request. At per-call API pricing, the cost scales linearly with usage. At a thousand checks a day, you're paying real money every month for something your CPU can do in milliseconds. At ten thousand checks a day, the API bill is engineering's salary.
The build cost was real — domain rules in a lib/matching/ module
(nakshatra-data, dosham, south-indian, profile-matcher, preference-matcher)
plus a service layer with lazy compute and a pair-keyed cache. Probably
two weekends. After that the marginal cost of compatibility checks is
roughly zero, and I get to ship Smart Match without worrying about a
third-party rate limit.
This is the inverted version of "build vs buy" that founders need to internalise: when the thing is core to the product, build. When it's not, buy. The test isn't "is buying easier" — buying is always easier today. The test is "in twelve months, when this scales, is the recurring cost going to dominate the upfront build cost?" For a feature that runs on every interaction, the answer is yes. For a one-off integration, the answer is no.
I named this debt as "two weekends of build, paid back inside a month of real usage." It paid back faster than that.
The postbuild migration hook: a named footgun
In the build pipeline, there's a postbuild hook that automatically runs
tsx scripts/apply-migration.ts after every Vercel deploy. Schema migrations
ship with the code that depends on them. No manual step, no separate
deploy, no "did anyone remember to migrate."
This is a known footgun. A bad migration in a deploy means the deploy itself fails — there's no rollback path that's instant the way a code-only rollback is. Every deploy carries migration risk, even ones that don't touch the schema, because the script runs unconditionally.
I took the debt anyway, because the alternative — separate manual migration steps in a solo build, where I'm the only person who'd remember to run them — was the bigger risk. Forgetting to migrate would have cost me more, more often, than a bad migration breaking a deploy will cost me, less often.
The trigger for paying back this debt is named: when there's a second engineer, the migration step gets pulled out of postbuild and into a manual, gated, observable deploy step. Not before.
The pattern
The four decisions above look unrelated until you read them as the same question, asked four times: what am I willing to pay for what I'm trying to buy?
- Schema-per-tenant: paying operational complexity to buy isolation
- Two-app split: paying operational complexity to buy deploy isolation that I didn't actually need at solo scale
- Build the kundali engine: paying upfront engineering hours to buy long-term cost savings on a high-frequency operation
- Postbuild migration hook: paying deploy risk to buy never-forgetting-to-migrate
Three of these were appropriate; one was overpriced. The difference isn't that I made a mistake on the second one. The difference is that I noticed I was making a debt decision on the other three, and didn't notice on the second one until I started paying for it.
The actual rule
Every shortcut should have:
- A named owner. Future-me, on this date, will pay this. Not "the team" (there is no team). Not "later." A specific person, often me, on a specific kind of trigger.
- A named cost. What does carrying this debt cost per week, per deploy, per incident? If you can't write that down, you don't know what you're taking on.
- A named trigger. What event causes me to pay this back? Headcount growing past one? Tenant count past ten? A specific incident pattern?
If a shortcut doesn't have those three things, it's not a shortcut. It's a thing you decided to forget about, and forgetting is how solo builds collapse — not in a single explosion but in the slow accumulation of unnamed debts that nobody is tracking because nobody owns them.
The failure mode isn't taking on debt. It's taking it on accidentally.