Blog post

When a SaaS Stops Being About Features

Why building a real multi-tenant SaaS stops being a feature problem and becomes a problem of trust boundaries, guarantees, and responsibility placement.

When a SaaS Stops Being About Features

I thought I was solving a product problem.

I wanted an orders table. Show the data, make the UI work, wire the flow, and move on.

That was the obvious decision. The less obvious one was whether the system behind the table could make a promise about what each user was allowed to see.

Once I started building a real multi-tenant SaaS instead of a demo, the feature request split in two. One part asked whether the flow worked. The other asked whether the flow was safe to trust.

Those are different thresholds.

The first threshold is easy to reach

A modern stack makes the first threshold pleasantly accessible. Next.js gives the application structure. An ORM makes queries convenient. Auth primitives identify the user. A clean route and service split keeps the code readable.

That can get a feature surprisingly far.

The happy path renders. The order is fetched. The database write succeeds. The demo is convincing.

But “far enough to make the app work” is not the same as “far enough to make the system trustworthy.” The second threshold appears when the feature depends on a guarantee the UI cannot own.

Who is allowed to see this order? Which tenant is active? Where is that authority established? What happens if a write fails after the identity record has been created?

The table did not become less important. It revealed that the product was carrying more responsibility than the screen suggested.

The choice I had been postponing

There were two ways to proceed.

I could keep treating tenant scope as data that travels through the request: a value in a URL, a form, a query, or a function parameter. That path is fast and feels flexible. It also makes every downstream layer responsible for remembering not to trust the value too much.

Or I could make the boundary structural. Authentication would establish who is acting and which tenant is in scope. Request context would carry that decision immutably. Services would make business decisions from trusted context rather than reconstructing authority. Queries would include the scope because the operation requires it, not because a caller remembered a convention.

The second choice costs more up front. It reduces the number of convenient shortcuts. It makes internal paths less magical. It forces transactions around actions that used to look like several unrelated writes.

I chose it because the residual cost of the first option was not merely maintenance. It was uncertainty about whether the system could keep its promise when a request was malformed, copied, retried, or deliberately changed.

What the feature was actually asking for

A real SaaS feature rarely asks only for a screen. It asks for a bundle of guarantees:

  • this user acts within the right tenant;
  • authorization is interpreted consistently across paths;
  • dangerous operations cannot be reached through a friendly shortcut;
  • related writes either complete together or leave a state the system can explain;
  • the next engineer can see where policy lives.

None of those guarantees comes from a framework by default. Frameworks reduce the cost of implementation. They do not decide which layer owns authority or what must remain atomic.

That is why architecture patterns became more practical as the system became real. Clean Architecture, Hexagonal Architecture, and domain-oriented boundaries are not valuable because their diagrams look disciplined. They are useful when they stop a policy decision from leaking into a route handler, a repository, or the browser.

The test is not whether the code resembles a pattern. The test is whether misuse becomes harder and responsibility becomes visible.

The coordination cost is part of the product

There is another cost that is easy to miss when implementation gets faster: coordination.

Every shortcut creates a small amount of future interpretation. A tenant ID passed “just this once” becomes a convention someone must remember. An internal escape hatch becomes a path a reviewer has to inspect. A half-completed business action becomes state the next operation must repair.

Fast output can therefore increase the amount of system knowledge that has to be reconstructed during review. The more quickly features arrive, the less acceptable it is for their guarantees to live only in the builder’s memory.

That is not an argument for making every feature ceremonious. It is an argument for making the important boundaries explicit before the feature spreads them across the system.

What I would decide now

I would still ship the orders table. I would not pretend that shipping it proves the work is complete.

Before calling the feature done, I would ask:

  • Is tenant scope derived from authentication and immutable for the request?
  • Does every relevant operation carry that scope structurally?
  • Are authorization decisions owned by the right layer rather than repeated in the UI?
  • Are business-critical writes coherent under partial failure?
  • Are dangerous development paths constrained instead of merely discouraged?

If the answer is no, the product is not failing because it lacks another feature. It is carrying an unowned guarantee.

That is the point at which a SaaS stops being mainly about features. Not when the codebase becomes large, and not when someone gives the team an architectural title. It happens when users rely on the system to preserve a boundary that the feature itself cannot explain.

Features remain what the user sees. Guarantees are the cost of making that surface deserve trust.

Continue exploring

Follow the same line of thought through themes, tags, or a broader local search across the archive.

Keep following the thread.