Founder

What I learned shipping a multi-tenant SaaS mostly solo

The architecture calls I'd make again, the ones I'd undo, and why "boring" infrastructure was the best decision I made while building Forzive.

Building a multi-tenant SaaS by yourself is a series of bets against your own future time. Every clever abstraction is a debt you pay down later, alone, usually at 11pm. After taking Forzive from an empty repo to gyms running their whole operation on it, here's what actually held up — and what I'd do differently.

Boring infrastructure was the best decision I made

The temptation, solo, is to reach for the shiniest stack so the work feels modern. I went the other way: one Postgres database, one Next.js app, one deploy. No microservices, no message queues I didn't need, no Kubernetes. When something broke, there was exactly one place to look.

Multi-tenancy lives in the data layer, not the architecture. Every tenant is a row-scoped slice of the same database, and a single middleware resolves the tenant from the subdomain. That's it. The "interesting" version with a database per tenant would have buried me in migrations and connection management for zero early benefit.

Solo means you are the on-call rotation. Optimize for the 2am debugging session, not the architecture diagram. Every moving part is a part you get paged for.

Enforce tenant isolation at the data layer, not by discipline

The scariest bug in multi-tenant software is a query that forgets its WHERE tenantId = ?. You cannot rely on remembering it on every call — at some point you won't. I scoped tenancy in one place: a query helper that refuses to run without a tenant context. If isolation is structural, a tired version of you can't leak data across gyms.

Pair that with seed data that contains two tenants from day one. Most cross-tenant leaks hide until a second tenant exists; if your dev database always has two, the bug surfaces while you're building, not after a customer reports it.

What I'd undo

  • Premature settings. I made things configurable that every tenant left on the default. Each toggle was UI, storage, and a branch in the logic — for nothing. Ship the opinion; add the setting when a customer asks twice.
  • Hand-rolled background jobs. My first version of recurring billing was a cron script with home-grown locking. It mostly worked, which is the worst outcome — it failed rarely and silently. A real job runner from the start would have saved a month of mystery.
  • Skipping observability. I added structured logging late. Before that, "is it slow for everyone or one gym?" was unanswerable. The first thing I'd install next time is per-tenant timing.

The meta-lesson

Solo, your scarcest resource isn't compute or money — it's the version of you six months from now who has to understand this code. Every decision is really a decision about that person's time. Boring, structural, two-tenants-in-the-seed boring — that's what keeps them sane.

Build the system you can still operate on your worst day, not the one that looks best on a whiteboard.
More articlesBuilding something? Let's talk