<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Chiankem Macbrain </title>
    <description>The latest articles on DEV Community by Chiankem Macbrain  (@macsystemsdev).</description>
    <link>https://dev.to/macsystemsdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4059395%2F690e0856-37fa-4a6c-a013-bf076a7b7c9f.png</url>
      <title>DEV Community: Chiankem Macbrain </title>
      <link>https://dev.to/macsystemsdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/macsystemsdev"/>
    <language>en</language>
    <item>
      <title>I Thought Permissions Were Simple. Then Teams Showed Up.</title>
      <dc:creator>Chiankem Macbrain </dc:creator>
      <pubDate>Mon, 03 Aug 2026 08:23:07 +0000</pubDate>
      <link>https://dev.to/macsystemsdev/i-thought-permissions-were-simple-then-teams-showed-up-2gh5</link>
      <guid>https://dev.to/macsystemsdev/i-thought-permissions-were-simple-then-teams-showed-up-2gh5</guid>
      <description>&lt;p&gt;I thought permissions would be one of the easiest parts of building TaskForge.&lt;/p&gt;

&lt;p&gt;Create roles. Assign permissions. Done. Ship it, move to the next feature.&lt;/p&gt;

&lt;p&gt;That assumption survived until the moment a second person joined a project. Then every action became a question nobody had actually answered yet: who can assign this? Who can archive it? Who can reopen something that's already marked complete?&lt;/p&gt;

&lt;p&gt;The code was never the hard part. Agreeing on the rules was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with a flat role check
&lt;/h2&gt;

&lt;p&gt;TaskForge is multi-tenant: Organization → Workspace → Team → Project. Early on, the natural instinct is something like:&lt;/p&gt;

&lt;p&gt;if ($user-&amp;gt;role === 'manager') {&lt;br&gt;
    // allow the action&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;That works right up until a single user is a Manager in one workspace and just a Member in another. A flat, global role check has no way to represent that — the same person needs different standing depending on where they are, not just who they are. Permissions turned out to be contextual, not global, and that realization changed the rest of the architecture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkor3yaeo1u5pz054g9eh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkor3yaeo1u5pz054g9eh.png" alt="Before and after comparison: a flat role check like if role equals manager, versus a context-aware permission check that includes the project" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Separating roles from permissions
&lt;/h2&gt;

&lt;p&gt;Instead of hardcoding role checks everywhere, we split the concepts apart. A role grants permissions — it isn't itself the check.&lt;/p&gt;

&lt;p&gt;Manager&lt;br&gt;
  ↓&lt;br&gt;
project.create&lt;br&gt;
project.update&lt;br&gt;
project.archive&lt;/p&gt;

&lt;p&gt;Rather than sprinkling if ($role === 'manager') across the codebase, every action checks for a specific permission. Roles become a convenient bundle of permissions, not the source of truth for what's allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Killing magic strings
&lt;/h2&gt;

&lt;p&gt;Once permissions became strings like 'project.create' and 'project.archive', they started showing up everywhere — controllers, policies, Blade views. Any typo silently fails. Any rename means hunting through the whole codebase.&lt;/p&gt;

&lt;p&gt;The fix was a dedicated permission constants class:&lt;/p&gt;

&lt;p&gt;class ProjectPermissions&lt;br&gt;
{&lt;br&gt;
    const CREATE = 'project.create';&lt;br&gt;
    const UPDATE = 'project.update';&lt;br&gt;
    const ARCHIVE = 'project.archive';&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Small change. Big payoff — autocomplete, refactor safety, and one source of truth instead of a string scattered across the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping policies thin
&lt;/h2&gt;

&lt;p&gt;This was probably the decision with the most leverage. It's tempting to let a Policy do everything — check permissions, run business logic, maybe touch the database. We stopped that early, before Policy classes turned into 300-line files.&lt;/p&gt;

&lt;p&gt;A Policy answers exactly one question: is this allowed? Nothing else. The actual how — creating the project, updating records, firing events — moved into dedicated Action classes: CreateProjectAction, UpdateProjectAction, DeleteProjectAction. Permissions decide whether something can happen. Actions decide how it happens. Mixing those two concerns is what turns authorization code into something nobody wants to touch six months later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhca0x24sr7t4mdk9t3go.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhca0x24sr7t4mdk9t3go.png" alt="Diagram showing the authorization flow from Team to Role to Action to Policy to Business Rule" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Authorization has to know where it is
&lt;/h2&gt;

&lt;p&gt;The real fix wasn't just separating roles from permissions — it was making every check context-aware:&lt;/p&gt;

&lt;p&gt;$user-&amp;gt;canProjectPermission(&lt;br&gt;
    ProjectPermissions::UPDATE,&lt;br&gt;
    $project&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;"Can this user edit a project?" is an incomplete question. The real question is: can this user edit this project, inside this workspace, under this organization? Because a project belongs to a workspace, which belongs to an organization, authorization naturally follows that same hierarchy instead of checking each level independently — which is exactly the kind of inconsistency that creeps in if you're not careful.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frek75wv0w2mcs2g75tx9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frek75wv0w2mcs2g75tx9.png" alt="Diagram showing the authorization hierarchy from User to Team to Project to Policy to Decision" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake that cost the most time
&lt;/h2&gt;

&lt;p&gt;Early on, it felt reasonable to give every team its own role: Owner, Admin, Manager, Team Lead, Developer, QA, Guest. Within a week, nobody — including me — could reliably say which role actually had which permission.&lt;/p&gt;

&lt;p&gt;Roles should stay few. Permissions grow. That imbalance is easy to miss until you're staring at seven roles trying to remember what Team Lead can do that Developer can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;None of this was a Laravel problem. Laravel's authorization primitives (can(), Gates, Policies) are fine on their own. The lesson was architectural: permissions aren't a user-management feature bolted onto the side of an app. They're part of the domain model. Treat them like middleware and they'll hold up for a while, then quietly become unmaintainable the moment real teams and real hierarchy show up. Treat them like a business capability from the start, and the system scales with the organization instead of fighting it.&lt;/p&gt;

&lt;p&gt;I'm building TaskForge — an operational collaboration platform for teams and agencies — in public, and writing/recording about the actual engineering decisions behind it as they happen. No tutorials, no toy examples — just what broke and what we changed because of it.&lt;/p&gt;

&lt;p&gt;If you've hit a version of this — permissions that looked simple until real usage exposed the gaps — I'd like to hear how you solved it.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>architecture</category>
      <category>backend</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
