<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://imhamzaazam.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://imhamzaazam.github.io/" rel="alternate" type="text/html" hreflang="en-US" /><updated>2026-11-11T06:13:26+05:00</updated><id>https://imhamzaazam.github.io/feed.xml</id><title type="html">Hamza Azam</title><subtitle>A computer science graduate exploring startups, fintech, technology, and occasionally breaking things just to see what happens.</subtitle><author><name>&lt;strong&gt;Hi,&lt;/strong&gt; I&apos;m Hamza.</name><email>im.hamzaazam@gmail.com</email></author><entry><title type="html">Building Rbac Multi Tenant Saas</title><link href="https://imhamzaazam.github.io/building-rbac-multi-tenant-saas/" rel="alternate" type="text/html" title="Building Rbac Multi Tenant Saas" /><published>2026-11-11T00:00:00+05:00</published><updated>2026-11-11T00:00:00+05:00</updated><id>https://imhamzaazam.github.io/building-rbac-multi-tenant-saas</id><content type="html" xml:base="https://imhamzaazam.github.io/building-rbac-multi-tenant-saas/"><![CDATA[<hr />
<p>layout: post
title: “Building RBAC for a Multi-Tenant SaaS: Permissions, Roles, and Scopes”
date: 2026-11-11 06:00:00 +0500
tags: [backend, rbac, saas, software-architecture]
description: “How I’m approaching role-based access control for a multi-tenant freight-forwarding SaaS.”
comments: false
—</p>

<h1 id="building-rbac-for-a-multi-tenant-saas-permissions-roles-and-scopes">Building RBAC for a Multi-Tenant SaaS: Permissions, Roles, and Scopes</h1>

<p>Authentication is usually the easy part. A user logs in, gets a token, and starts using the application.</p>

<p>Authorization is where things get interesting.</p>

<p>I’m building a multi-tenant freight-forwarding SaaS, and one of the things I’ve been thinking carefully about is how users should access different parts of the system.</p>

<p>At first, role-based access control (RBAC) sounds straightforward. Create a few roles, assign permissions, and you’re done.</p>

<p>But real businesses aren’t that simple.</p>

<p>A tenant might have multiple companies. Each company might have multiple branches. Someone working in one branch shouldn’t automatically have access to everything happening in another.</p>

<p>And then there’s the platform itself, which has its own users and responsibilities.</p>

<p>I wanted an authorization model that could handle these requirements without becoming unnecessarily complicated.</p>

<p>Here’s the approach I’m currently taking.</p>

<h2 id="1-separating-authentication-from-authorization">1. Separating authentication from authorization</h2>

<p>I’m using Keycloak for authentication and PostgreSQL as the source of truth for application permissions.</p>

<p>I don’t want to build password management, login flows, token issuance, and identity management myself. That’s what an identity provider is for.</p>

<p>Keycloak handles:</p>

<ul>
  <li>User authentication</li>
  <li>Password management</li>
  <li>Login flows</li>
  <li>Access token issuance</li>
  <li>Identity information</li>
</ul>

<p>My application handles:</p>

<ul>
  <li>Users and tenant membership</li>
  <li>Roles and permissions</li>
  <li>Role assignments</li>
  <li>Company and branch access</li>
  <li>Authorization decisions</li>
</ul>

<p>The distinction is intentional.</p>

<p>A valid access token tells me who the user is. It doesn’t automatically mean they’re allowed to perform every action in the application.</p>

<p>I want application permissions to remain under my control, rather than duplicating the entire permission model inside Keycloak.</p>

<h2 id="2-understanding-the-hierarchy">2. Understanding the hierarchy</h2>

<p>Before designing the tables, I needed to be clear about the structure of the application.</p>

<p>The hierarchy I’m working with is:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Tenant
  └── Company
       └── Branch
</code></pre></div></div>

<p>A tenant can have multiple companies, and each company can have multiple branches.</p>

<p>This matters because access can be assigned at different levels.</p>

<p>For example:</p>

<ul>
  <li>A user might have access to an entire company.</li>
  <li>Another user might only have access to one branch.</li>
  <li>A platform user might manage platform-level operations independently of tenant access.</li>
</ul>

<p>I don’t want to solve these requirements by creating a separate role for every possible combination.</p>

<p>That would become difficult to maintain very quickly.</p>

<p>Instead, I’m treating <strong>permissions, roles, and scope as separate concepts</strong>.</p>

<h2 id="3-permissions-describe-what-a-user-can-do">3. Permissions describe what a user can do</h2>

<p>A permission should describe an action, not where that action is allowed.</p>

<p>For example:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>shipment:create
shipment:read
shipment:update
shipment:delete

invoice:view
invoice:create

user:create
user:read
user:update
</code></pre></div></div>

<p>These permission codes answer one question:</p>

<p><strong>What is the user allowed to do?</strong></p>

<p>They don’t answer which company or branch the user can access.</p>

<p>I deliberately want to keep scope out of permission names.</p>

<p>I don’t want permissions such as:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>shipment:read:company
shipment:read:branch
shipment:read:tenant
</code></pre></div></div>

<p>That mixes two different responsibilities.</p>

<p>The permission says what action is allowed. The assignment determines where that permission applies.</p>

<p>This keeps the permission catalog relatively small and easier to manage.</p>

<h2 id="4-roles-group-permissions">4. Roles group permissions</h2>

<p>Creating individual permissions for every user would be difficult to manage.</p>

<p>That’s where roles come in.</p>

<p>A role is simply a collection of permissions.</p>

<p>For example:</p>

<p><strong>Operations</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>shipment:create
shipment:read
shipment:update
</code></pre></div></div>

<p><strong>Accountant</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>invoice:view
invoice:create
</code></pre></div></div>

<p><strong>Sales</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>customer:create
customer:read
quotation:create
quotation:read
</code></pre></div></div>

<p>A user receives permissions through their assigned roles.</p>

<p>I’m also planning to support both system-defined roles and custom roles created by tenants.</p>

<p>System roles provide sensible defaults, while custom roles let businesses adapt the application to their own workflows.</p>

<p>Both should use the same permission catalog. There’s no reason to introduce a completely separate permission system for custom roles.</p>

<h2 id="5-scope-belongs-to-the-role-assignment">5. Scope belongs to the role assignment</h2>

<p>This is probably the most important decision in my current design.</p>

<p>A role tells us what a user can do. An assignment tells us where they can do it.</p>

<p>Consider this example:</p>

<p>Hamza has the Operations role.</p>

<p>Does that mean he can manage shipments across every company and branch?</p>

<p>Not necessarily.</p>

<p>He might only be responsible for the Dubai branch of a particular company.</p>

<p>Another user with exactly the same Operations role might have access to the entire company.</p>

<p>The role is identical. The scope is different.</p>

<p>My current assignment model looks something like this:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">user_role</span> <span class="p">(</span>
    <span class="n">id</span> <span class="n">UUID</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="k">DEFAULT</span> <span class="n">uuidv7</span><span class="p">(),</span>
    <span class="n">tenant_id</span> <span class="n">UUID</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">REFERENCES</span> <span class="n">tenant</span><span class="p">(</span><span class="n">id</span><span class="p">),</span>
    <span class="n">company_id</span> <span class="n">UUID</span> <span class="k">NULL</span> <span class="k">REFERENCES</span> <span class="n">company</span><span class="p">(</span><span class="n">id</span><span class="p">),</span>
    <span class="n">branch_id</span> <span class="n">UUID</span> <span class="k">REFERENCES</span> <span class="n">branch</span><span class="p">(</span><span class="n">id</span><span class="p">),</span>
    <span class="n">user_id</span> <span class="n">UUID</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">REFERENCES</span> <span class="n">user_account</span><span class="p">(</span><span class="n">id</span><span class="p">),</span>
    <span class="n">role_id</span> <span class="n">UUID</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">REFERENCES</span> <span class="k">role</span><span class="p">(</span><span class="n">id</span><span class="p">),</span>
    <span class="n">created_at</span> <span class="n">TIMESTAMPTZ</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">DEFAULT</span> <span class="n">NOW</span><span class="p">()</span>
<span class="p">);</span>
</code></pre></div></div>

<p>The important part is <code class="language-plaintext highlighter-rouge">company_id</code> and <code class="language-plaintext highlighter-rouge">branch_id</code>.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">branch_id IS NULL</code> means the assignment applies to the entire company.</li>
  <li>A specific <code class="language-plaintext highlighter-rouge">branch_id</code> means the assignment applies only to that branch.</li>
  <li><code class="language-plaintext highlighter-rouge">company_id IS NULL</code> means the assignment applies to entire tenant and <code class="language-plaintext highlighter-rouge">tenant_id</code> should not be null.</li>
</ul>

<p>Notice that this represents company-level and branch-level assignments. It does not represent tenant-wide access across multiple companies.</p>

<p>If I need tenant-wide assignments, I’ll model that explicitly rather than treating a missing branch ID as tenant-wide access.</p>

<p>This separation gives me flexibility without creating unnecessary role variations.</p>

<h2 id="6-the-frontend-selects-context-but-the-backend-authorizes-access">6. The frontend selects context, but the backend authorizes access</h2>

<p>In the frontend, users need a way to select the company and branch they’re currently working in.</p>

<p>I’m thinking of handling this through request headers:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">x-company-id: &lt;company-id&gt;
x-branch-id: &lt;branch-id&gt;
</span></code></pre></div></div>

<p>For example, a user might belong to multiple companies and switch between them using a company selector.</p>

<p>The selected company and branch represent the user’s current working context.</p>

<p>But there’s an important distinction here:</p>

<p><strong>A frontend-selected context is not proof of authorization.</strong></p>

<p>A user can modify request headers. So the backend must validate that the selected company and branch belong to the tenant and that the user’s assignments permit access to that context.</p>

<p>The frontend tells the backend where the user wants to work. The backend decides whether they’re allowed to work there.</p>

<p>I prefer this approach because it keeps the frontend simple while keeping authorization decisions on the server.</p>

<h2 id="7-keeping-nestjs-authorization-simple">7. Keeping NestJS authorization simple</h2>

<p>I don’t want every controller to contain its own authorization logic.</p>

<p>That would quickly lead to duplicated checks and inconsistent behavior.</p>

<p>Instead, I’m planning to use a small permission decorator:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">RequirePermission</span><span class="p">(</span><span class="dl">'</span><span class="s1">shipment:read</span><span class="dl">'</span><span class="p">)</span>
<span class="p">@</span><span class="nd">Get</span><span class="p">()</span>
<span class="nf">findAll</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">shipmentService</span><span class="p">.</span><span class="nf">findAll</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The decorator declares what permission an endpoint requires.</p>

<p>A central authorization layer handles the actual decision:</p>

<ol>
  <li>Identify the authenticated user.</li>
  <li>Resolve the user’s role assignments.</li>
  <li>Check whether the required permission is available.</li>
  <li>Validate the selected company and branch against the assignment scope.</li>
  <li>Allow or reject the request.</li>
</ol>

<p>The decorator shouldn’t need to know whether the user has company-wide or branch-specific access.</p>

<p>That’s not its responsibility.</p>

<p>It only declares the required permission.</p>

<p>The authorization layer handles the rest.</p>

<h2 id="8-authorization-must-reach-the-database">8. Authorization must reach the database</h2>

<p>Passing an authorization check doesn’t mean the database can return everything.</p>

<p>The data access layer still needs to enforce the correct tenant, company, and branch boundaries.</p>

<p>For example, a branch-specific shipment query might look like this:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span>
<span class="k">FROM</span> <span class="n">shipment</span>
<span class="k">WHERE</span> <span class="n">tenant_id</span> <span class="o">=</span> <span class="err">$</span><span class="mi">1</span>
  <span class="k">AND</span> <span class="n">company_id</span> <span class="o">=</span> <span class="err">$</span><span class="mi">2</span>
  <span class="k">AND</span> <span class="n">branch_id</span> <span class="o">=</span> <span class="err">$</span><span class="mi">3</span><span class="p">;</span>
</code></pre></div></div>

<p>For a company-wide assignment, the branch condition would be handled differently because the user is allowed to access multiple branches within that company.</p>

<p>This is important because authorization shouldn’t depend only on whether an endpoint is accessible.</p>

<p>The actual records returned by the database must also respect the user’s scope.</p>

<p>I’m keeping these filters close to the data access layer so that they aren’t accidentally forgotten in individual endpoints.</p>

<h2 id="9-separating-platform-and-tenant-access">9. Separating platform and tenant access</h2>

<p>The SaaS platform itself has users with responsibilities that are different from tenant users.</p>

<p>For example, platform users might handle:</p>

<ul>
  <li>Platform ownership</li>
  <li>Finance</li>
  <li>Customer support</li>
</ul>

<p>Tenant users, on the other hand, work inside their own business environment.</p>

<p>I don’t want these two contexts mixed together.</p>

<p>My current direction is to keep platform and portal identities separate using distinct Keycloak realms:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">REALM</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">PLATFORM</span><span class="p">:</span> <span class="dl">'</span><span class="s1">platform</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">PORTAL</span><span class="p">:</span> <span class="dl">'</span><span class="s1">portal</span><span class="dl">'</span><span class="p">,</span>
<span class="p">}</span> <span class="kd">as const</span><span class="p">;</span>
</code></pre></div></div>

<p>The application can then resolve the correct user context and authorization rules based on the realm.</p>

<p>Platform permissions and tenant permissions remain separate concerns.</p>

<p>This also avoids giving platform users tenant-level access just because they have a platform role.</p>

<h2 id="10-what-im-trying-to-avoid">10. What I’m trying to avoid</h2>

<p>While designing this, there are a few things I deliberately don’t want to introduce.</p>

<p><strong>Hardcoding roles everywhere</strong></p>

<p>Roles should be manageable data, not assumptions scattered throughout the codebase.</p>

<p><strong>Putting scope inside permission names</strong></p>

<p>Permissions should describe actions. Assignments should describe access boundaries.</p>

<p><strong>Trusting frontend context</strong></p>

<p>Headers help select the working context, but the backend must validate every selection.</p>

<p><strong>Duplicating authorization logic</strong></p>

<p>Controllers shouldn’t each implement their own permission and scope checks.</p>

<p><strong>Overengineering too early</strong></p>

<p>I don’t need a complicated policy engine or a distributed authorization service to get started. A clear permission catalog, role assignments, and centralized authorization checks should give me a solid foundation.</p>

<p>The goal is to keep the design flexible without building more than the product currently needs.</p>

<h2 id="final-thoughts">Final thoughts</h2>

<p>I’m still building this system, so these decisions will evolve as the application grows.</p>

<p>But I want to get the fundamentals right early.</p>

<p>The model I’m working towards is fairly simple:</p>

<table>
  <thead>
    <tr>
      <th>Concept</th>
      <th>Responsibility</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>User</td>
      <td>Who is making the request?</td>
    </tr>
    <tr>
      <td>Permission</td>
      <td>What can they do?</td>
    </tr>
    <tr>
      <td>Role</td>
      <td>Which permissions are grouped together?</td>
    </tr>
    <tr>
      <td>Assignment</td>
      <td>Which role does the user have?</td>
    </tr>
    <tr>
      <td>Scope</td>
      <td>Where does that role apply?</td>
    </tr>
    <tr>
      <td>Context</td>
      <td>Which company or branch are they currently working in?</td>
    </tr>
  </tbody>
</table>

<p>Keeping these concepts separate makes the authorization model easier to reason about.</p>

<p>And more importantly, it gives me room to support different business structures without turning every new requirement into another special case.</p>

<p>That’s the direction I’m taking for RBAC in my freight-forwarding SaaS.</p>

<hr />

<h2 id="references">References</h2>

<ul>
  <li><a href="https://csrc.nist.gov/projects/role-based-access-control">NIST: Role Based Access Control</a></li>
  <li><a href="https://www.keycloak.org/documentation">Keycloak Documentation</a></li>
  <li><a href="https://www.postgresql.org/docs/current/ddl-rowsecurity.html">PostgreSQL Row Security Policies</a></li>
</ul>]]></content><author><name>&lt;strong&gt;Hi,&lt;/strong&gt; I&apos;m Hamza.</name><email>im.hamzaazam@gmail.com</email></author><summary type="html"><![CDATA[layout: post title: “Building RBAC for a Multi-Tenant SaaS: Permissions, Roles, and Scopes” date: 2026-11-11 06:00:00 +0500 tags: [backend, rbac, saas, software-architecture] description: “How I’m approaching role-based access control for a multi-tenant freight-forwarding SaaS.” comments: false —]]></summary></entry><entry><title type="html">Hello, Friend.</title><link href="https://imhamzaazam.github.io/hello-friend/" rel="alternate" type="text/html" title="Hello, Friend." /><published>2026-11-10T18:00:00+05:00</published><updated>2026-11-10T18:00:00+05:00</updated><id>https://imhamzaazam.github.io/hello-friend</id><content type="html" xml:base="https://imhamzaazam.github.io/hello-friend/"><![CDATA[<p>Hello, friend.</p>

<p>Funny how we end up here, isn’t it?</p>

<p>You, on the other side of the screen. Me, putting words out into the void.</p>

<p>Anyway, welcome. Have a look around. You might find something interesting.</p>]]></content><author><name>&lt;strong&gt;Hi,&lt;/strong&gt; I&apos;m Hamza.</name><email>im.hamzaazam@gmail.com</email></author><category term="personal" /><summary type="html"><![CDATA[Hello, friend. Been expecting you.]]></summary></entry></feed>