SaaS architecture
Three decisions in a SaaS product you never get to undo
Customer separation, money and permissions. What each of the three costs up front, what it costs afterwards, and how to check your own product in ten minutes.
In a SaaS product almost everything is reversible. You rebuild a screen, you version an API shape, you replace a framework if you really must, and you change a pricing model with a migration and an email. That is precisely what makes software attractive to start: you do not have to get it right in one go.
Three things fall outside that. Together they cost about a working week up front, and afterwards none of the three can still be straightened out cleanly. This is the overview: what the decision is, what it costs up front, what it costs afterwards, and how to check in ten minutes where your own product stands. Behind each of the three sits an article that goes deeper than this overview does.
Which decisions in a SaaS product cannot be undone later?
Three: how you keep customers apart, how you store and divide amounts of money, and how you model permissions. They are irreversible because the repair touches not just your code but your production history — data that has been at the wrong customer, invoices already out the door, and users nobody can still say which role they actually had. Up front they cost around four to five days of work between them; afterwards each of the three is a project no customer wants to pay for.
The three in one table
| Decision | The classic mistake | Up front | Afterwards | How you notice it went wrong |
|---|---|---|---|---|
| Separating customers | a tenant_id column with only a filter in the application | ± two days | weeks, plus lasting uncertainty about whether you caught everything | a report, export or background job shows another customer’s rows |
| Calculating money | amounts in a float or double | ± one day | you migrate not just your data model but your history | the customer adds it up and lands a cent away from you |
| Modelling permissions | a list of checkboxes per user | one to two days | guessing, for every existing user, which role they actually had | “which boxes do I tick to make this work again?” |
1. Separating customers: two locks, not one
The usual approach is a tenant_id column on every table and a filter in the application. That works until it does not — and the holes are always in the same four places: a raw query for a report that was too slow, a background job with no logged-in user, an import or export that bypasses the ordinary layer, and a relation that walks one step too far.
What does hold up is a second lock in the database itself: row-level security in PostgreSQL, where the session sets a single variable and the policy on the table refuses the rest. A raw query then gets zero rows, a background job without context gets nothing, and neither does an import that bypasses the ORM. That is the whole point: the second layer protects you from the mistakes you are going to make in the first.
In full: multi-tenant from day one.
2. Calculating money: cents, not floats
The wrong number type raises no error. It produces an answer that looks right and is occasionally a cent off, and that deviation travels upward through invoice lines, tax and totals until the balance no longer balances. The damage is not the cent but the trust.
Store amounts as whole cents in an integer, with the currency beside them. And know that integers solve addition but not division: the moment you split an amount there is a remainder somebody has to receive, and that is a product decision which belongs written down somewhere.
In full: money in cents, not floats.
3. Modelling permissions: roles, not checkboxes
Checkboxes per user start as a reasonable answer to a reasonable request and seize up around the twentieth customer in three directions at once: no longer testable, no longer explainable, no longer migratable. Permissions belong to a role, drawn from a fixed and small set named after what someone can do rather than after the screen it sits on.
One distinction comes with that which even experienced teams put in the wrong place: a role decides what someone may do, not what it applies to. That second half is the customer boundary, and it belongs in the database — otherwise the separation between two companies still hangs off an if in the application.
In full: permissions: roles, not checkboxes.
Check your own product in ten minutes
Four checks, and all four are in your own hands. You need no audit for them and no access to anybody’s source but your own.
1. Turn the customer context off and see what comes back. Run an arbitrary query with no logged-in user — from a console, a report or a background job. The right answer is zero rows. If you see data, your customer separation hangs off a single lock, and that lock lives in code somebody will bypass one day.
2. Search for the word float in the columns that hold amounts. One hit is enough to know. If there is a decimal type, check whether there is one function that divides — and whether a test asserts that the sum of the lines is exactly the total.
3. Calculate the same invoice twice, with the lines in a different order. If a different amount comes out, your split depends on the order of your data and will give a different answer after the next migration than before it. This is the test almost nobody writes and the one that most often finds something.
4. Count the endpoints in your application and count the rows in your permission matrix. Different numbers mean there is an action nobody attached to a role. Have that test pull the list of endpoints out of the application itself rather than typing it over: a matrix you maintain by hand covers exactly last year’s code.
Why these three
The line is sharper than it looks. A decision is reversible as long as the repair only touches your code. These three touch your history, and then the question is no longer how do we convert it but which of the two answers was true — the stored total or the sum of the lines, the role somebody had or the checkboxes that are there now. That is no longer a technical question, and there is nobody who can answer it for you.
On top of that, none of the three produces anything you can show a customer. All three are entirely invisible for as long as they are right, and that is exactly why they rarely get the attention they deserve. We had to decide all three up front for booxx and Pilot-Next, including the times we nearly got them wrong.
Starting something new? This is the first thing we look at when we build a SaaS product. Already have something running and one of the four checks above will not let you go? Then the conversation usually turns to moving an existing platform forward. Either way: send us an email — half an hour is usually enough to know whether it fits.