Skip to content

SaaS Roles and Permissions: How to Design Them From Day One

SaaS roles and permissions done right: a simple role model that can grow, why a hardcoded admin flag gets expensive, and 7 steps to design access early.

By

Freelance full-stack developer

Published
Reading time
12 min
In this post8

Good SaaS roles and permissions start with one rule: your code should ask what a user is allowed to do, not which role they have. Give every customer account three or four fixed roles (owner, admin, member and maybe a read-only viewer), store the role on the user's membership of that account, and have the code check named permissions like "can export invoices" instead of "is admin". That setup costs almost nothing extra on day one, and it can grow into customer-defined roles later without a rewrite.

The opposite, a single is_admin flag checked all over the codebase, is a common shortcut in early SaaS products. It's also one of the most expensive to undo.

The short answer

Access control in a SaaS product tends to grow through three levels. You don't need the top one at launch, but whatever you build first has to be able to get there.

Three levels of SaaS roles and permissions
Level 1: Fixed rolesLevel 2: Fixed roles, permission checksLevel 3: Custom roles
How it worksEach user has one role per account, such as owner, admin or memberEach role is a bundle of named permissions, and the code checks the permissionThe customer's admin builds their own roles from a list of permissions
Where the rules liveIn codeIn codeIn the database, per account
EffortLowLow, if done from the startMedium to high: needs a UI, tests and support docs
Good fit forPrototypes and internal toolsMost B2B SaaS products at launchLarger customers with many users, departments or procurement requirements
Typical mistakeRole names checked directly throughout the codeToo many permissions too earlyBuilt before anyone asked for it

My recommendation for almost every new product is level 2. Users only see a handful of fixed roles, but under the hood every check is a permission check. It costs about the same to build as level 1, and it's the only one of the first two levels that grows into level 3 without a rebuild.

Roles are one of a few foundations that are cheap to get right early and painful to change later. The others are covered in my complete guide to building a SaaS.

The vocabulary: users, accounts, memberships and permissions

Most permission messes start when four separate ideas get squeezed into one column on the users table:

  • Authentication answers who you are: email and password, a magic link, or single sign-on (SSO) through the customer's identity provider.
  • Account (often called a tenant or workspace) is the customer that pays, usually a company.
  • Membership links one user to one account. This is where the role lives.
  • Authorization answers what you may do inside that account: view invoices, invite colleagues, change the plan.

The membership is what makes real-world setups possible. An agency's project manager might be an admin in the agency's own workspace and a member in three client workspaces. An external bookkeeper might have read-only access to a dozen companies. With the role on the user record, your system can't express either.

Accounts and memberships are tightly linked to how customer data is kept apart. Roles decide what people inside one company can do. Keeping companies away from each other's data is a separate mechanism, which I've covered in multi-tenant SaaS architecture.

One more boundary: your own support agents are not "admins" in a customer's account. That's a different kind of access with its own rules, covered in step 6.

The hidden cost of a hardcoded admin flag

The quickest way to ship permissions is an is_admin column and an if-statement wherever something needs protecting. It works until real customers show up with real org charts.

The check ends up everywhere

The first customer who asks for "someone in finance who can see invoices but can't manage users" sends you hunting through the whole codebase. Every admin check needs a decision, and missing one means shipping either a bug or a security hole.

In code the difference looks tiny:

if ($user->is_admin) {
    // show the button and allow the action
}

if ($user->can('export', Invoice::class)) {
    // show the button and allow the action
}

The first asks about a role. The second asks about a permission, and which roles hold that permission is defined in one place. In Laravel those places are gates and policies, and they ship with the framework.

One role per user breaks at the second account

The moment a user needs to belong to two accounts, the data model has to change and existing data has to be migrated. It tends to be one of the slowest refactors in a SaaS codebase, because it touches login, invitations, emails and every place that looks up "the current account".

Your super admin shares a flag with the customer's admin

If your support team and a customer's admin use the same flag, a single bug is all that stands between one customer and everyone else's data. When I find this in a codebase, it's the first thing I recommend fixing, before any new features go on top.

The UI is doing the security

Hiding a button protects nothing. If a user can call the API directly, or change an ID in the URL from invoice 1041 to 1042, the server has to refuse. This class of bug is everywhere. The security nonprofit OWASP ranks broken access control number one in its Top 10:2025 list of critical web application risks, and every application tested in its dataset had some form of it. My web application security checklist covers the related holes.

A starter role model you can grow

This is the model I usually recommend as a starting point for a B2B SaaS: four roles per account, built from a short list of permissions.

Example starter model: owner, admin, member and viewer
PermissionOwnerAdminMemberViewer
View data in the accountYesYesYesYes
Create and edit own recordsYesYesYesNo
Edit other people's recordsYesYesNoNo
Export dataYesYesNoNo
Invite and remove usersYesYesNoNo
Change plan and billingYesNoNoNo
Delete the account or transfer ownershipYesNoNoNo

Treat it as an example, not a template. A few design choices make any version of it hold up:

  • The owner is special. There must always be at least one owner, and only owners touch billing or delete the account. Build an ownership transfer flow early, because the person who signed up is not always the person who stays at the company.
  • Permissions are named after actions, such as invoices.export or users.invite. They live in code, so they're versioned, reviewed and tested like everything else.
  • Record ownership is its own rule. "Can edit invoices" means invoices in your own account, and for a member perhaps only the ones they created. OWASP puts it as access controls that enforce record ownership instead of letting users act on any record of a type.
  • Plans gate features, and roles gate people. If exports are only on the top plan, that's something the subscription grants the account, not something baked into a role. Keep the two separate and a pricing change never forces a role change.

When the model needs to grow, do it in small steps. First more fixed roles, such as a finance role with invoice access. Then access per project or department if customers organize that way. Custom roles come last. If you sell through resellers, you'll also get a layer above your customers, with roles like partner admin and partner support, which I cover in the post on white-label SaaS.

How to design roles and permissions in 7 steps

  1. List actions, not people. Walk through the product screen by screen and write down every verb: view, create, edit, delete, export, invite, pay. That's your permission list. Keep it short, and don't create permissions for things users can't do yet.
  2. Group them into three or four roles based on your first customers. Ask who in their company will use the product, and who must never be able to delete anything. Their answers beat a role model copied from another tool.
  3. Put the role on the membership. One table that links user, account and role, even if nobody belongs to more than one account today.
  4. Make the code check permissions, never role names. Roles are bundles of permissions, defined in one place. Spatie, who maintain a widely used Laravel package for roles, give the same advice in their docs: check permissions, not roles.
  5. Enforce on the server, centrally, with deny by default. Every request goes through the same check, typically a policy or middleware (a layer that runs before the action itself). If no rule grants access, the answer is no. The OWASP Authorization Cheat Sheet is built on the same principles: least privilege, deny by default and a check on every request.
  6. Give your own team separate access. Support needs to help customers, but through a dedicated admin panel or an "impersonate user" feature that only a few people can use, with every session logged. EU customers tend to ask about this, and it's part of the GDPR checklist for SaaS.
  7. Test permissions automatically. Write tests that prove a member can't invite users, a viewer can't edit, and a user in account A can never see account B's data. Run them on every change, so a new feature can't quietly open a hole.

Also log role changes and invitations: who made whom an admin, and when. Larger customers often ask for this in security questionnaires, and you'll want it the day something gets deleted and nobody knows by whom.

Roles and permissions: pre-launch check

  • The role is stored on the membership between user and account, not on the user.
  • The code checks named permissions and never compares role names directly.
  • Every request is authorized on the server, including API calls and direct file downloads.
  • Access is denied when no rule explicitly allows it.
  • A user can never see or change data in another account, and an automated test proves it.
  • Every account always has at least one owner, and ownership can be transferred.
  • Your support team's access is separate from customer roles, and every use is logged.
  • Role changes and invitations are logged.

When to let customers build their own roles

Custom roles look great in a feature list, but they're rarely the first thing customers miss. Advanced role management is on my list of features your MVP can skip at launch for that reason.

It's time to build them when you see one or more of these signals:

  • Several customers ask for the same in-between role, each with small variations, and fixed roles no longer cover it.
  • Accounts have many users spread across departments, offices or countries with different needs.
  • Larger customers list granular access control as a requirement in tenders or vendor security reviews.

It's too early if you only have a few customers, if a single customer asked, or if accounts have a handful of users each. One customer's request is usually better solved with a new fixed role that others might use too.

Custom roles take more than a new table. The customer's admin needs a UI that explains what each permission does. The system must stop anyone from granting more access than they hold, and stop the last owner from locking themselves out. And support needs a way to answer "why can't this person see that page?". Build level 2 properly first, and level 3 becomes an extension, not a rewrite.

When roles aren't the whole answer

Selling to larger European companies often brings another requirement: they want to manage access from their own identity provider, such as Microsoft Entra ID or Okta, so a leaver loses access the day they leave. That's one more reason to keep authentication and roles separate.

Some products outgrow roles as the main model. When access depends on relationships, like only seeing the projects you're assigned to, roles get combined with rules on the data itself, which Laravel policies handle well. If the same rules have to be shared across many services and teams, look at a dedicated authorization service. At that scale it's a job for an organization with a platform team, not a single freelance developer like me.

Next steps

If you're planning a SaaS, decide on roles during the discovery phase, together with the data model and tenant isolation. Write down the actions, pick three or four roles, and agree with your developer that the code checks permissions from day one. It's a decision that costs hours now and can save weeks later.

Already running on is_admin? Fix it step by step: centralize the permission definitions, move the checks over one module at a time, then lock it down with tests.

To see how I work on SaaS products, from a fixed-price discovery phase to ongoing development after launch, take a look at my SaaS development service.

Frequently asked questions

What's the difference between RBAC, ABAC and ReBAC?

RBAC (role-based access control) grants access based on a user's role, such as admin. ABAC (attribute-based) uses properties of the user, the data or the context, like department or region. ReBAC (relationship-based) grants access through relationships, such as being assigned to a project. RBAC is the easiest to reason about and fits most early SaaS products. Mature products often mix all three.

Should I use a library or build permissions myself?

Use what your framework already offers and only design the role model yourself. In Laravel, gates and policies are built in, and for fixed roles defined in code they're often all you need. If roles have to be stored and edited in the database, possibly per account, spatie/laravel-permission is a common choice and supports teams. Whatever you pick, never hand-roll authentication or session handling.

Does GDPR require role-based access control?

GDPR doesn't mention roles directly. Article 32 requires security appropriate to the risk, and Article 25 requires that by default personal data is only accessible to the extent necessary. Your customer is usually the controller and you're the processor, so they need your product to let them restrict access. Roles are the most common way to do that. This isn't legal advice, so check with a privacy lawyer for your specific case.

What should happen to data when a user is removed from an account?

Remove the membership, not the data. Records belong to the account rather than the individual, so invoices, tasks and notes stay put and the person's name remains in the history. Access, however, must end immediately, including active sessions and API tokens. If the user only belonged to that one account, the user record itself can then be deactivated or deleted according to your retention rules.

A customer wants a role nobody else needs. What do I do?

Build it as a new fixed role with a generic name if other customers are likely to use it. Avoid special-case code for one named customer, because it sticks around and makes every future change more expensive. When several customers each want their own variation, that's your signal to build roles customers can configure themselves.