Software is famously easy to change, which is true of most of it. The exceptions are the decisions that live underneath everything else, and changing those after you have paying customers means migrating live data while people are using the system.
Three of them are worth deliberate thought before the first feature is built.
1. Tenant isolation
How do you keep one customer's data from another's? There are three broad answers, and they are not interchangeable.
- Shared schema with a tenant identifier. One database, one set of tables, a
tenant_idcolumn on every row. Cheapest to operate and simplest to deploy. The risk is a single missing WHERE clause exposing one customer's data to another, so the isolation has to be enforced in a shared query layer rather than remembered by each developer. - Schema per tenant. Stronger isolation, straightforward per-customer backup and restore. Migrations now run hundreds of times, and that becomes an operational discipline of its own.
- Database per tenant. Strongest isolation, easiest story for customers with data residency requirements, most expensive to run. Usually justified only for enterprise contracts that pay for it.
Choose against your compliance obligations and your realistic customer size, not against what a blog post recommends in the abstract. Then enforce it in code, because a convention that relies on everyone remembering will eventually be forgotten.
2. Billing
Subscription billing looks simple until you enumerate the cases: trials that convert and trials that lapse, mid-cycle upgrades with proration, downgrades that should take effect at renewal, seat changes, annual plans with monthly usage limits, failed payments and the dunning sequence, refunds, tax by jurisdiction, and the webhook that arrives twice.
Two rules save most of the pain. First, use an established payment provider and let it own the hard cases rather than reimplementing them. Second, treat the provider as the source of truth for subscription state and reconcile from its webhooks, rather than keeping a parallel copy in your own database that drifts.
The failure mode to avoid is billing logic scattered through the codebase, added case by case as each situation arose. That is the part of a SaaS codebase nobody volunteers to work on.
3. The permission model
Early products often have one kind of user. Then a customer asks for a read-only account for their accountant, and another wants an approver who can authorise but not create, and another wants a parent organisation that can see several child accounts.
Retrofitting roles into a system built for one user type touches every screen and every query. Designing for roles from the start costs relatively little: define permissions as capabilities rather than job titles, check them in one place, and make the check part of the query layer rather than a condition in the template.
What can safely wait
Almost everything else. Interface polish, the second integration, the reporting suite, the mobile app, even most of the feature list. These are all reversible, and building them before you know what customers actually use is how effort gets spent on the wrong things.
The distinction worth holding onto is between decisions that are cheap to revisit and decisions that are not. Move quickly on the first kind. Slow down for the second.