Building SaaS Products: Lessons from 12 Real Projects

Hard-won lessons from building 12 SaaS products -- covering architecture decisions, auth patterns, billing pitfalls, and the mistakes we stopped repeating.

Software developer working at desk with multiple monitors
Photo by Christina Morillo on Pexels

Twelve Products, Twelve Sets of Scars

Over the past several years we have built, shipped, and maintained twelve SaaS products. Some hit the ground running — Mindhyv became a functioning therapist-client platform within weeks. Others taught us expensive lessons about what happens when you skip steps or make assumptions. Trackelio, LancerSpace, Vincelio, and the rest each added something new to our playbook.

What follows is not generic startup advice. These are specific, technical lessons that came from shipping real code, handling real users, and fixing real outages. If you are building a SaaS product or thinking about it, this is the stuff nobody warns you about until it’s too late.

Multi-Tenant Architecture: Pick Your Model Early

The first major decision in any SaaS build is how you isolate tenant data. We have used three approaches across our twelve projects, and each has trade-offs that become permanent.

Shared database, shared schema is the cheapest to operate. One database, one set of tables, a tenant_id column on everything. We used this for LancerSpace because the data model was uniform across tenants and the priority was speed to market. It works — until one tenant’s query patterns slow down the database for everyone. You also need to be ruthless about enforcing that tenant_id filter in every single query. Miss one, and you have a data leak.

Shared database, separate schemas gives you better isolation without the operational overhead of multiple databases. We moved Trackelio to this model after realizing that different clients needed slightly different reporting structures. Each tenant gets their own schema, which makes migrations more complex but gives you room to customize.

Separate databases per tenant is the cleanest from an isolation standpoint but the most expensive to maintain. We reserve this for products where data sensitivity demands it, like Mindhyv, where therapist-patient records require strict separation.

The lesson: decide your tenancy model before you write your first migration. Changing it later means rewriting your data access layer, and in two cases that cost us more time than the original build.

Authentication Patterns That Scale

We stopped rolling our own authentication after project number three. The amount of surface area in auth — password hashing, session management, token rotation, email verification, password reset flows, rate limiting on login attempts — is staggering. Every time we built it from scratch, we spent weeks on code that added zero product value.

Now we default to established providers and focus our effort on the authorization layer, which is where the actual product logic lives. Who can see what? Who can edit what? What happens when a user belongs to multiple organizations?

The pattern that has worked best for us is role-based access control with resource-level permissions. A user has a role within a tenant (admin, member, viewer), and specific resources can override those defaults. When we built Vincelio, this let us implement a system where profile owners could grant granular access to specific sections of their data without requiring admin intervention.

The mistake we see constantly: treating authentication and authorization as the same problem. Auth tells you who someone is. Authorization tells you what they can do. Confusing the two leads to either security holes or a permissions system so rigid that users can’t get their work done.

Billing Integration Will Humble You

If auth is underestimated, billing is actively hostile. We have integrated Stripe into eight of our twelve products, and every single time there is a new edge case we didn’t anticipate.

Here’s what catches people: billing is not a single event. It’s a state machine. A subscription can be active, past due, canceled, paused, trialing, or in a grace period. Each state has different implications for what the user can access. Your application needs to handle every state transition, including the ones that happen via Stripe’s dashboard when a support agent manually adjusts something.

Webhooks are where it gets dangerous. Stripe sends events, but they can arrive out of order, they can be duplicated, and if your endpoint goes down, they queue up and hit you all at once when you come back. We learned to build our webhook handlers to be idempotent and order-independent. Every webhook event gets logged, deduplicated, and processed against the current state rather than assuming a linear sequence.

The other pitfall: proration. When a user upgrades mid-cycle, downgrades, adds seats, or changes plans, the billing math gets complicated fast. We now build a billing service layer that abstracts all of this away from the rest of the application. The app asks “can this user do this?” and the billing service answers. The app never touches Stripe directly.

The MVP Trap: Both Directions

Everyone talks about building too much in an MVP. Ship fast, validate, iterate. That part is right. But we have also seen the opposite problem: building too little and calling it an MVP.

When we first built an early version of one project, we shipped with such a bare-bones feature set that users couldn’t actually complete the core workflow. They could start the process but not finish it. That’s not an MVP — that’s a demo. An MVP needs to deliver one complete value loop. The user should be able to go from having a problem to having it solved, even if the solution is manual in places.

With Mindhyv, the MVP was tight: a client could take an intake assessment, get matched to a therapist, and book an initial session. That was it. No messaging, no billing, no document sharing. But that single loop — assessment to match to booking — was complete and valuable. Everything else came later based on what real users actually asked for.

Build the smallest thing that delivers complete value. Not the smallest thing you can ship, and not the full vision either.

Database Schema Decisions You Can’t Undo

Some database decisions are easy to change. Adding a column, creating a new table, adjusting an index — these are routine. But certain schema decisions become load-bearing walls that you cannot remove without rebuilding the house.

The big ones:

  • Your primary key strategy. UUIDs versus auto-incrementing integers versus something else. This affects your URLs, your API design, your join performance, and your ability to merge data across environments. We default to UUIDs now because they make multi-tenant data handling and environment syncing dramatically easier.
  • Your soft-delete strategy. Once you start soft-deleting records, every query in your application needs to account for it. We’ve had bugs where a “deleted” record showed up in reports because one query forgot the WHERE deleted_at IS NULL filter. Decide this at the start and enforce it at the ORM level, not in individual queries.
  • Your audit trail approach. If you need to know who changed what and when, retrofit it later is painful. We now include created_by, updated_by, and a separate audit log table from the first migration. The cost is minimal upfront but the alternative — adding it after the fact to a production database with millions of rows — is a project unto itself.
  • Your approach to polymorphic relationships. If a “comment” can belong to a task, a project, or a document, how do you model that? The decision you make here affects query complexity for the entire life of the product.

Monitoring From Day One, Not Day Ninety

We used to add monitoring as an afterthought. The product would launch, run fine for a few weeks, and then something would break at 2 AM and we’d have no idea what happened because we had no logs, no alerts, and no metrics.

Now, monitoring goes in during the first sprint. Not complex monitoring — just the basics. Error tracking so we know when things break. Uptime monitoring so we know when things go down. Request logging so we can trace what happened when a user reports a problem. Database query performance so we can catch slow queries before they become outages.

For Trackelio, we caught a database connection pool exhaustion issue during the first week of beta because we had query performance monitoring active from launch. Without it, we would have discovered the problem when the app crashed under load — probably during a client demo.

The rule we follow now: if you wouldn’t launch without authentication, don’t launch without monitoring. They’re both infrastructure, and they’re both non-negotiable.

Bottom Line

Building SaaS products is an exercise in making decisions with incomplete information and living with the consequences. The lessons above didn’t come from reading blog posts — they came from debugging production issues, rewriting systems we built wrong the first time, and watching real users interact with software we thought was finished.

Every one of our twelve projects made us better at this. The architecture decisions are faster now, the billing integrations are smoother, and the monitoring is in place before the first user ever logs in. But the core lesson hasn’t changed: respect the complexity, ship the smallest complete thing, and build the infrastructure to know when something goes wrong.

If you’re planning a SaaS build and want to skip the painful lessons, let’s talk. We’ll help you make the right architectural decisions from the start.

Let's build something great Let's build something great Let's build something great Let's build something great Let's build something great