Permissions are the one part of a SaaS build that founders consistently underestimate, then spend enormous amounts of money fixing later. Not authentication – that problem is well understood. Permissions. Who can see what, who can do what, and how that changes as your product adds teams, plans, and enterprise customers who each have their own ideas about access control. Get it wrong in the first build and you’re looking at $30,000-$80,000 in rework before you hit scale, plus the product risk of shipping a breaking change to live customers.
The mistake we see most often is treating permissions like a feature rather than a data model. A developer adds an is_admin boolean to the user table, ships it, and for six months it’s fine. Then a customer asks for a “view-only” role. Then another wants team leads who can approve but not configure. Then an enterprise prospect won’t sign without custom roles. At that point, the boolean is embedded in forty-plus conditional checks across the codebase and you’re not refactoring – you’re doing surgery on a moving patient.
Start With the Model, Not the UI
The first decision is architectural, and it needs to happen before you write a single permissions-related line of UI code. There are three main models worth considering for a SaaS product:
- Role-Based Access Control (RBAC): Users are assigned roles, roles are assigned permissions. Simple, predictable, the right choice for most early-stage SaaS products.
- Attribute-Based Access Control (ABAC): Permissions are computed from attributes on the user, the resource, and the environment. Powerful and flexible, but genuinely complex to reason about and debug. You don’t want this unless you have a specific use case that RBAC cannot handle.
- Relationship-Based Access Control (ReBAC): Permissions are defined by relationships between entities – think Google Docs’ sharing model or how Linear handles teams. If your product has nested resources or highly granular sharing, this is worth understanding.
For most Australian SaaS founders, the answer is RBAC – but built with enough structure to extend cleanly. What that actually means in practice is defining your permission model around resources and actions from day one, not around roles. Roles become named bundles of resource-action pairs. When a customer asks for a new role, you’re just creating a new bundle. You’re not touching core logic.
The Scope Problem Nobody Talks About
Here’s the question that separates a permissions model that lasts from one that collapses: at what level does access control apply?
You have a platform-level scope (Amora the company is a customer), an organisation scope (Amora’s marketing team is a sub-group), and a resource scope (this specific report belongs to this specific user). Most SaaS products need at least two of these. Many need all three. And the scope hierarchy you design in month one will follow you for the life of the product.
A fintech we shipped last year started with organisation-level RBAC – three roles, clean and simple. Six months post-launch, enterprise customers started asking for team-level permissions and resource-level sharing. Because we’d built the data model with explicit scope columns and a polymorphic permission assignment table from the start, adding those layers took a sprint, not a quarter. If we’d hardcoded roles against a single organisation_id, we would have been rebuilding the foundation.
The design decision looks like this in practice: instead of storing user_id → role, you store user_id → role → scope_type → scope_id. A user can be an admin at the organisation level and a viewer at a specific project level. That’s a three-column change in your schema and a modest increase in your permission evaluation logic – but it keeps every future requirement open.
Where to Put the Enforcement Logic
This is where teams create the most technical debt. Permissions checks end up scattered: some in controllers, some in service layers, some in database queries, some in the frontend. Then a bug slips through because one path missed a check. Then you’re doing a security audit trying to trace every surface.
The pattern we reach for is a centralised policy layer – one place in the application where permission decisions are made. In a Rails stack, that’s Pundit policies. In a Node/TypeScript stack, we often build a thin, explicit policy module that wraps all resource access. The API layer calls the policy layer before executing any business logic. The policy layer calls nothing else. It takes a user, a resource, and an action, and returns allow or deny. That’s it.
This approach has a few non-obvious benefits. First, your permission logic becomes testable in isolation. You can write unit tests against the policy layer without spinning up API infrastructure. Second, when a customer reports an access issue, you have one place to look. Third, when you add a new role or a new resource type, you’re adding a new policy, not hunting through existing code to figure out where to insert checks.
Frontend permissions are a different concern. Hiding UI elements based on permissions is good UX but it is not security. The API must enforce permissions independently. We see this confused more often than you’d expect – teams who believe that removing a button is sufficient access control. It is not. It is never sufficient.
What Multi-Tenancy Actually Does to Your Permissions Model
If you’re building a multi-tenant product – and most SaaS products are – you need row-level security or its equivalent working before you worry about roles at all. A permissions model that doesn’t guarantee tenant isolation is a liability, regardless of how elegant the RBAC structure is.
At the database layer, this typically means every significant table has a tenant_id column with an index, and your query layer always filters by the current tenant context. Some teams use PostgreSQL’s Row Level Security to enforce this at the database level – it adds a layer of safety but also complexity in how you manage policies and bypass rules for administrative operations. We generally prefer application-level enforcement with strong conventions and test coverage over RLS unless the threat model specifically demands it.
The failure mode worth being paranoid about is the “confused deputy” problem – where an authenticated user from tenant A crafts a request that retrieves or modifies data belonging to tenant B, because the API accepts a resource ID without verifying tenant ownership. This is a straightforward class of bug to introduce and a damaging one to discover post-launch. Your permission checks must always verify that the resource being acted on belongs to the requesting tenant, not just that the user has the right role.
The Cost Reality of Getting This Wrong
If you ship an is_admin boolean and need to replace it with a proper RBAC model six months later, you’re probably looking at two to four weeks of a senior developer’s time – call it $15,000-$25,000 at agency rates – plus QA, migration scripts, and a potentially difficult deployment. Do it at eighteen months with a larger codebase and the number climbs. Do it after an enterprise customer has specific contractual requirements about permissions and you’re doing it under pressure with a deal on the line.
Building a proper model from the start adds roughly two to five days to an MVP build. In our 28-day sprints, that’s real time, and we’re direct about the trade-off with founders. The right call for a pure validation MVP where you’re testing whether anyone cares about the product at all might be to ship the simple version intentionally, document the debt, and revisit it before you go to market seriously. What’s never the right call is treating the simple version as permanent and being surprised when it isn’t.
If you’re at that decision point – whether to build permissions properly from the start, or how to clean up a model that’s already outgrown itself – talk to Amora about your build. It’s a contained problem with a clear solution, and it’s much easier to address before your user table has fifty thousand rows in it.
A Practical Checklist Before You Ship Permissions
- Is your permission model defined around resources and actions, not hardcoded role strings?
- Does your scope design support at least two levels – organisation and resource – even if you only use one today?
- Is there a single, testable policy layer that all permission decisions flow through?
- Does every resource lookup verify tenant ownership, not just role membership?
- Are your frontend permission checks cosmetic, with the API enforcing independently?
- Have you written tests that attempt cross-tenant access and verify it fails?
Six questions. If you can answer yes to all of them before you go live, you’ve built something that won’t humiliate you in front of an enterprise prospect or require a rebuild when your second pricing tier lands. Permissions aren’t glamorous. Neither is the work of fixing them under pressure. Build them right the first time and you’ll stop thinking about them entirely, which is exactly the goal.
Got something you want built?
Amora Digital is an Australian software and AI agency. We scope it, build it, and ship it – live in 28 days. No offshore teams. No surprises.