← All posts

Roles and permissions in a business app: who can do what

Most multi-user apps fail first on access control. Here is how to design roles and permissions so staff, customers and admins each see the right screens and nothing they should not.

Author
Elie Chaaban
Published
Reading time
8 min
Topics
Mobile Apps, Web Apps, Product, Security, Backend

Every business app with more than one kind of user eventually asks the same question: who is allowed to see this screen, change this record, or approve this action? Get the answer wrong and you ship either a free-for-all (every staff member can export everything) or a maze of special cases that break every time you add a feature.

Roles and permissions are not a polish item. They shape your data model, your API, your admin panel and your support load. This post is a practical way to design them for a booking product, a field-service platform, a CRM or any other multi-user app, without drowning in enterprise IAM jargon.

What "roles and permissions" actually means

A permission is a single capability: create a booking, assign a technician, view payroll, export customers, delete a location. A role is a named bundle of permissions that matches a real job: customer, technician, dispatcher, branch manager, owner.

Users are assigned one or more roles. When they hit an endpoint or open a screen, the backend checks whether any of their roles grant the required permission. The mobile or web UI should hide actions they cannot perform, but the backend must still enforce the rule. Hiding a button is not security.

That separation matters. Founders often describe access in UI terms ("the technician app should not show pricing"). What you need in the product is a permission such as jobs.view_pricing, granted to dispatchers and owners, denied to technicians. The technician app then simply never receives the field.

Start from jobs, not from a permission matrix

Blank grids with fifty checkboxes look thorough and produce the wrong design. Start from the people who will use the product and what they do in a normal day.

For a cleaning booking product like Pro Clean+, the cast is usually:

For a field-service platform like NeonTask, the cast is different:

Write those roles down in plain language before you open a spreadsheet of permissions. If two roles do almost the same work, merge them for the first version. Extra roles are cheap to add later and expensive to untangle when every feature has five special cases.

A simple model that scales past MVP

For most business apps, three layers are enough:

  1. Authentication: who is this person? Email and password, magic link, OTP, or SSO later. Covered in more depth in web app security basics.
  2. Authorization by role: which role(s) do they have in this account or tenant?
  3. Resource scope: even with the right role, which rows can they touch? A technician sees their jobs. A branch manager sees their branch. An owner sees the whole company.

Resource scope is where many MVPs go wrong. A permission of jobs.read without a scope check means any authenticated technician who guesses a job ID can read another crew's job, including the customer address and notes. Scope belongs in the query: "jobs assigned to this user", "jobs in this branch", "jobs in this tenant". Multi-tenant products need the tenant ID on every sensitive query; I wrote about the wider architecture choice in multi-tenant or one deployment per client.

Keep the permission names stable and boring: bookings.create, bookings.cancel, users.invite, reports.export. Avoid names tied to screens ("can_open_green_button"). Screens change. Capabilities last.

What belongs in the first version

For an MVP you rarely need fine-grained permissions per field. You need:

You usually do not need in v1:

Those features appear when a real customer hits a real wall. Until then they slow the build and confuse testing. Scoping the first version is the same discipline as how to scope an MVP: ship the roles you can explain on a whiteboard.

Where permissions show up in the product

API and backend

Every protected route declares the permission it needs. Middleware resolves the user, loads roles, and rejects with 401 (not signed in) or 403 (signed in, not allowed). Do not rely on the mobile app or SPA to filter alone. Anyone can call your API with a stolen or guessed token.

Return boring error bodies. "Forbidden" is enough for the client. Log the user id, permission and resource id server-side so support can debug "why can't Fatima assign this job?" without reading the code.

Mobile and web UI

Hide or disable actions the user cannot perform, based on a permissions payload you send at login or on a /me endpoint. Re-fetch that payload when roles change. Never hard-code "if role === technician" in twelve screens if you can check jobs.assign instead; role names change, permission checks stay readable.

Admin panel

The admin panel is where role mistakes hurt most. Exports, refunds, price edits and user invites should be permission-gated and, for the riskiest actions, audited (who did it, when, on which record). If two people share the owner password, you do not have roles. You have a secret that will leak.

Common design traps

One shared login for the whole shop. It blocks accountability, breaks when someone leaves, and makes device-level session revocation painful. Give people named accounts from day one, even if the first version only has two roles.

Permissions only in the frontend. Looks fine in a demo. Fails the first time a curious user opens the network tab or a former contractor still has a valid token.

Role explosion. "Senior cleaner Beirut", "Senior cleaner Jounieh", "Junior cleaner weekends" are not roles. Location and seniority are attributes or separate scopes. Roles should stay few and stable.

Confusing authentication with authorization. Resetting a password does not change what the user can do. MFA does not replace a permission check. They solve different problems.

Letting any admin create another owner. Decide early who can grant which roles. Often only an owner can create owners, and only owners can export full customer lists. Put that rule in the backend, not in a wiki.

No offboarding path. Deactivating a user should kill refresh tokens, remove them from assignment pools and keep their historical actions attributable. Deleting the row and cascading into "unknown user" on old jobs makes audits useless.

How this interacts with the rest of the stack

Roles sit next to the same foundations every serious product needs: a clear API for the mobile backend, auth hardened along the lines of web app security basics, and an admin desk that operations can actually run. On a single-server Docker setup they are just another set of tables and middleware checks; the hard part is the product decisions, not the library you pick.

If you use an LLM feature later (summaries, document extraction, suggested replies), apply the same permission checks to the data you send the model. A technician should not be able to prompt-summarise another branch's jobs because the AI endpoint forgot to filter by scope. The rule is the same as any other read path.

A concrete starter set

Copy this and adapt names to your domain:

| Role | Can typically do | Cannot typically do | |---|---|---| | Customer | Manage own bookings or requests | See other customers, staff lists, pricing rules | | Field worker | Update assigned jobs, upload media | Reassign jobs, change prices, invite users | | Dispatcher / ops | Assign work, contact customers, manage schedule | Change billing plan, delete the account | | Admin / owner | Users, roles, settings, exports, refunds | Impersonate without an audit trail (avoid this) |

Add a fifth role only when a real workflow has no home. For a CRM such as the custom build behind Enchanté, you might split "front of house" and "manager" once those jobs diverge; you do not need that split on day one if one person still does both.

Testing access control without a drama

Write a short matrix: role × critical action × expected result (allow / deny). Automate the API cases. Manually click through the mobile and admin UIs for each role before every release that touches auth. Include one negative test per role: a technician requesting DELETE /users/123 must get 403.

On staging, keep one named account per role with a password in your team vault. "Log in as the technician user" should take thirty seconds, not a rebuild. That habit pairs well with a real staging environment.

When to revisit the design

Revisit roles when you hire a second branch, sell to a company that needs managers and staff, or get asked for "view only" access by an accountant. Those are product signals, not reasons to redesign every month. Resist rebuilding the matrix for every sales call; note the request, ship the core workflow, then generalise when the second customer asks for the same thing.

If you are taking over an inherited app and nobody can explain who can delete what, treat access control as week-one work. Map current users to jobs, lock down exports and deletes first, then tidy the role names. That fits the wider approach in taking over an inherited codebase.

Getting it built

Clear roles turn a pile of screens into a product a team can run. They are part of how I design booking apps, field-service platforms and custom CRMs, alongside the API, admin panel and deployment. You can see that mix across the work page, including Pro Clean+ and NeonTask, or read about how I work.

If you are defining who should see what in an app you are about to build, or fixing an existing product where every staff login is shared, email me or book a call and we can sketch the three or four roles you actually need, and leave the fifty-row matrix for later.

More from the blog

Let's work
together