SaaS — Apr 22, 2025 — 6 min read
The SaaS Features That Look Free But Cost You a Rewrite
Four SaaS features that look like an afternoon of work. Each one has caused a rewrite when added after launch.

Every SaaS rewrite I have seen was caused by the same category of mistake. Not a bad framework choice. Not wrong infrastructure. A feature that looked like two days of work when added on day one but required touching every layer of the codebase when requested on month six.
Four features do this consistently. They are not exotic. Most SaaS products need all of them.
Multi-tenancy
The version of multi-tenancy you build in week one is a userId check on every database query. It works for a single user. It fails the moment a second person joins the same account, or a user belongs to multiple organizations, or an admin needs to view another tenant's data.
The real data model for multi-tenancy is not a column — it is a relationship. Every resource belongs to an organization. Users belong to organizations with roles. Queries filter by organization ID, not user ID. If your schema does not have an organizations table from the start, adding one later means migrating every table that ever stored user-scoped data.
In Supabase, Row Level Security makes this tractable if designed upfront. Retrofitting RLS policies onto an existing schema that assumed single-user ownership is the most expensive afternoon I have spent on a client project.
Role-based access control
RBAC looks like a role enum on the user table: admin, member, viewer. This covers the demo. It does not cover the product.
What breaks it: a member who should be able to edit their own resources but not others'. An admin who can access billing but not engineering settings. A viewer in one workspace who is an admin in another. These are not edge cases — they are the second conversation you have with any non-trivial client.
The permissions model that survives this is resource-level, not role-level. A role is a named bundle of permissions. A permission is a verb-noun pair: create:invoice, read:report, delete:member. The check is not if (user.role === 'admin') — it is if (can(user, 'delete:member')).
Building a role-level check first and then migrating to resource-level permissions mid-product means touching every protected route, every API handler, and every conditional UI element.
Audit logs
No one asks for audit logs in the MVP brief. Everyone asks for them after the first incident.
"Who deleted that record?" is not a question your application can answer without an audit log. "When did this setting change?" is not a question your database can answer without one either.
The architectural problem is that audit logging is a cross-cutting concern. It touches every mutation in the system. If you add it later, you are not adding a feature — you are adding a layer. You either wrap every database write manually (fragile), use database triggers (tied to a specific vendor), or build a middleware-level interceptor (requires a consistent write path).
The consistent write path — every mutation going through a service layer, never a direct ORM call from a controller — is the prerequisite. If your codebase skipped the service layer to ship faster, adding audit logs will expose that decision.
Billing edge cases
Stripe's happy path is fifteen lines of code and a weekend. Billing edge cases are three months of slow fires.
The ones that restructure your data model: prorated upgrades mid-cycle when the user has already used 80% of their quota. Downgrade timing — does the user lose access immediately or at period end? Failed payment retry behavior when the user is mid-session. Team billing where one member's seat is paid by a different card than the account owner.
Each of these requires a clear concept of what the user's current entitlements are, independent of what Stripe says their plan is. If your access control reads directly from the Stripe subscription object, you have no buffer when a webhook is delayed or a payment is disputed.
An entitlements table — owned by your application, synced from Stripe — is the correct abstraction. It is also the one most MVPs skip.
The pattern
All four of these share the same root cause. They are architectural constraints that look like features. Adding them after launch does not extend your data model — it replaces the assumptions your data model was built on.
Conclusion
Multi-tenancy, RBAC, audit logs, and billing edge cases are not features to add when clients ask for them. They are decisions that shape the schema, the service layer, and the access control model from the first migration. The cost of adding them late is not a sprint — it is a rewrite.
Summary
Multi-tenancy requires an organizations table and resource-scoped queries from day one; retrofitting it onto user-scoped data is a full migration. RBAC needs resource-level permissions, not a role enum, or the second client breaks it. Audit logs require a consistent write path that most MVPs skip. Billing edge cases need an entitlements table independent of Stripe. All four look like features. All four are architecture.

