Stage 2 · First Version · Chapter 12

User Roles and Permissions in SaaS: Build Real Locks, Not Curtains

A permission switch that only hides a button is not security. See how roles, switches and scope work in a SaaS, and how to enforce each one in the database.

Dr. Yahia Elkhouli · November 1, 2026 · 6 min read

In August 2026 I asked a simple question about one of our customers, a distribution company with several sales reps on the road. Earlier that summer, the owner had turned off the "discount" switch for all the reps. He did not want them giving discounts on their own. I wanted to know whether the switch was doing its job.

It was not. When we looked at the real data, hundreds of invoices created from the reps' own app carried a discount, and two of them had a discount of 100 percent. The goods left the warehouse for free. The switch had hidden the discount box on the rep's screen, and that was all it did. The server function that saved the sale accepted any discount it was given.

The owner was calm the whole time, because the switch said "off". That is the most dangerous kind of permission: one that looks like a lock and is really a curtain.

The short answer

A SaaS permission system has three ideas: a role that sets each person's ceiling, permission switches that can only lower that ceiling, and scope that decides which data a person works with. Hiding a button is user experience. Only a check inside the database is security, because anyone can skip the app and talk to the database directly.

Role, switch and scope

Every business app with more than one user must answer, for each person: what can they see, and what can they do? Three different ideas hide inside the word "permissions".

Enforcement works like a bank. Taking the sign off a door hides the room. Locking the door stops people walking in. A guard inside stops someone who got in another way. The vault has its own lock in case everything else fails. In software those are four layers: hide the menu item, block the address, guard inside the screen, and lock in the database. The first three live in the app and can be bypassed. Only the fourth is security. In Supabase, that means Row Level Security (rules on each table deciding which rows a user may read or change) and checks inside your database functions.

A permission that exists only on the screen is a suggestion, not a rule.

How we did it in KashBoss

Switches that only narrow

In June 2026 we made one firm decision: a switch can only remove things inside the role. It can never add power above it. We stored only the exceptions. If the owner turns off sales for one accountant, the employee's record holds one note: "sales: off". A switch with no note follows the role. On the day we shipped, nothing changed for any existing employee. Decide for each new switch whether it is open or closed by default, on purpose, every time.

THE PROMPT
I am building a business app with these roles: [LIST YOUR ROLES].
Design a permission system where each role has a fixed ceiling,
and the company owner can switch off individual actions per employee.
Rules: a switch can only remove, never add above the role.
Store only the switched-off actions, so existing employees are unchanged.
For each switch, tell me: default open or closed, and why.
Show me the full list grouped by menu section before writing any code.

The switches that did nothing

THE MISTAKE WE MADE

We shipped the first version of switches and moved on. In July 2026 I switched off a section for an accountant and nothing happened. The sidebar on the web and the menu on mobile never read the switches at all. They only checked the subscription plan. Accountants at several companies had switches saved by their employers, and not one of them worked. The first version was a screen full of toggles with no enforcement behind it.

We rebuilt it in three app layers: one central function that answers "is this allowed?", menus that hide the item when the answer is no, and a guard on every route. Then we found more ways a switch dies quietly. A switch can be defined in the catalog but missing from a role's ceiling, so no owner ever sees it. A role can be given a bigger ceiling without matching menu items. And one line of logic decides a lot:

canEdit = isAdmin OR switchIsOn    // wrong: every admin passes, the switch is ignored
canEdit = isAdmin AND switchIsOn   // right: the role decides who, the switch decides yes or no

With "OR", every manager passed before the switch was read. Worse, "admin" also included the manager role, so a switch meant to restrict managers was canceled for exactly the people it targeted. We now have an automatic test that fails if anyone writes "admin OR switch" again.

Put the lock in the database

The discount fix went into the server function that saves a rep's sale. Three details made it work. The function had an old and a new version, because older app copies on reps' phones still called the old one, so we guarded both. Allowed discounts still pass, because the owner had set automatic discount tiers on purpose, so the guard calculates what the rules allow and refuses only the extra discount typed by hand. And a rep who started the app offline worked with saved permissions, so the app now refreshes them as soon as the connection returns.

THE PROMPT
The switch [SWITCH NAME] currently hides [BUTTON OR FIELD] on screen only.
Enforce it inside the database function [FUNCTION NAME] as well.
Read the employee's permission from the database, not from the request.
Keep allowed automatic cases working: [LIST THEM].
Check if older versions of this function still exist and guard all of them.
Before changing anything, show me the current live version of the function.
Then test it: one call with the switch on, one with it off, and show both results.

Nobody edits their own power

Two holes appear in almost every permission system. First, an employee who can open the employees screen can change roles, so an accountant allowed in could promote himself to manager. Second, we let owners restrict a branch manager to certain warehouses, but stored the restriction on the manager's own record, and branch managers could edit employees. A restricted manager could remove his own restriction, or create a brand-new employee with no restriction and log in as that. A database trigger, a small rule that runs automatically when a row changes, now blocks both. The owner is exempt.

Hiding is not locking

In September 2026 a customer's owner reported that branch managers could see sales from every branch, although he had limited each to certain warehouses. Our restriction filtered the list of warehouses in the picker, but the screen opened on "all warehouses" by default, and that case fetched the whole company. For any restriction, ask one question: if someone skipped the app and talked to the database directly, would this rule still hold? Finally, test with a real account holding the target role, trying the action before the owner grants it and after. Chapter 12 of the book has the full checklist.

Key takeaways

Frequently asked questions

What is the difference between a role and a permission?

A role is a person's job and sets the maximum they can do. A permission switch is a single on/off setting inside that role, so an owner can narrow what one employee does without creating a new role.

Why isn't hiding a button enough for security?

Hidden buttons only change what the app shows. Anyone who calls the server or database directly can skip the screen, so the real check must run inside the database function that performs the action.

Should a new permission switch be open or closed by default?

It depends on the switch. Open by default means existing employees keep working with nothing missing. Closed by default fits sensitive features that nobody should use until the owner chooses. Decide deliberately and write down why.