RBAC is a business policy model, not a role column
In B2B SaaS, authorization becomes a domain problem as soon as one person can belong to several organizations, hold different responsibilities in each organization, act on resources with different ownership and delegate work to services or AI. A single role field on the user record cannot represent that model safely.
Start with the relationship between actor, organization and resource. Authentication answers who signed in. Authorization answers whether this actor, in this tenant context, may perform this action on this specific resource now.
Model the authorization primitives explicitly
| Primitive | Purpose | Example |
|---|---|---|
| User | Global human identity | [email protected] |
| Organization | Tenant / business boundary | Acme Europe |
| Membership | User relationship to an organization | Matt belongs to Acme Europe |
| Role | Convenient package of permissions | Billing manager |
| Permission | Durable business action | invoice.refund |
| Resource rule | Limits action to owned/specific objects | Only facilities in assigned region |
| Audit event | Evidence of privileged change | Who approved refund and why |
Enforce permissions on every server-side action
Frontend hiding is useful UX but not security. Every API mutation, server action, queue consumer and internal tool that changes protected state needs authorization at the execution boundary. The check should use server-derived identity and tenant context rather than accepting an arbitrary organization ID from the client.
For reads, use scoped repository/query helpers so the safe path is the default. For writes, load the target resource, prove its tenant/ownership boundary, evaluate permission and only then execute the domain command.
Roles package permissions; permissions describe business capabilities
Role names change between customers. Permissions should remain stable because they represent what the product can do: member.invite, booking.cancel, invoice.issue, asset.measurement.approve. A role such as “Manager” is simply a tenant-configurable set of those capabilities.
This makes migrations easier. New capability can be introduced as a permission and deliberately assigned to existing roles instead of silently giving every “admin” a new power.
Resource policy is where simple RBAC becomes real B2B authorization
Two users may both have order.update, yet one may only edit orders for an assigned branch or only orders that are not locked by finance. Resource-level policy can combine tenant, ownership, status, geography, assignment and contractual constraints with the permission.
Keep those rules in shared authorization/domain functions rather than duplicating them in controllers. Otherwise web, mobile, background jobs and AI tools will eventually enforce different policies.
Service accounts and AI actors need explicit semantics
Automation should not impersonate an omnipotent administrator. Give service identities a purpose, tenant scope and allowed capabilities. When an AI tool executes on behalf of a user, preserve both identities: the requesting human/session and the executing system actor. The audit event should be able to explain both.
This matters for production agentic systems because a semantically correct model decision is still invalid if the underlying actor would not have been allowed to perform it.
Audit privileged changes with before/after context
For membership, role, billing, export, deletion and other privileged actions, record actor, tenant, permission, target, timestamp, request/run correlation and the meaningful state change. Sensitive values can be redacted, but the audit record should make the decision reconstructable.
Auditing is not a replacement for authorization. It is the evidence layer that allows operators to understand how protected state changed after the authorization gate succeeded.
Database controls can add defense in depth
Application authorization should remain the primary policy layer, but database mechanisms such as PostgreSQL Row-Level Security can provide additional isolation for selected multi-tenant tables. RLS is strongest when tenant context is set reliably for every transaction and when privileged connections cannot accidentally bypass the intended policy.
Do not use RLS to hide an unclear domain model. Establish ownership and authorization semantics first, then decide where database enforcement reduces risk.
Negative-path tests are mandatory
Authorization tests should prove denial, not only success. Test cross-tenant IDs, removed memberships, stale sessions, downgraded roles, resources moved between scopes, queued jobs executed after permission loss and direct API calls that bypass the UI.
At Softech, these patterns sit underneath multi-tenant SaaS development and business web applications. The goal is not to create the most complex policy engine; it is to make the ownership model explicit enough that every interface enforces the same rules.
Migration path from simple roles
- Separate global user identity from organization membership.
- Introduce stable permission keys for high-value actions.
- Map current roles to permission sets without changing UX.
- Centralize server-side authorization helpers.
- Add resource/ownership policy where role-only checks are insufficient.
- Introduce audit events for privileged actions.
- Add negative-path and cross-tenant regression tests.
