惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - Franky
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
A
About on SuperTechFans
博客园 - 【当耐特】
Microsoft Security Blog
Microsoft Security Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
The GitHub Blog
The GitHub Blog
雷峰网
雷峰网
博客园_首页
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
IT之家
IT之家
博客园 - 叶小钗
Google DeepMind News
Google DeepMind News
aimingoo的专栏
aimingoo的专栏
博客园 - 聂微东
B
Blog RSS Feed
H
Help Net Security
Recent Announcements
Recent Announcements
阮一峰的网络日志
阮一峰的网络日志
D
DataBreaches.Net
L
LangChain Blog
Vercel News
Vercel News

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Building Dynamic RBAC in React 19: From Permission String...
Ugur Aslim · 2026-05-23 · via DEV Community

Ugur Aslim

Building Dynamic RBAC in React 19: From Permission Strings to Component-Level Access Control

String-based permission checks scattered across your React codebase are a maintenance nightmare. I know because I shipped CitizenApp with that anti-pattern, and it nearly bit me when we added our fifth AI feature.

The problem? Permissions were hardcoded in components. When the marketing team wanted to trial a feature with select customers, I had to grep through half the codebase, find every if (user.role === 'admin') check, and create some Frankenstein conditional. Worse, there was no single source of truth for what "feature_x_access" actually meant across our tenant hierarchy.

This post shows you how to build a type-safe, composable RBAC layer that lives outside your components. Your UI asks "can I do X?" and the permission engine answers. Clean separation. Testable. Scales.

The Core Philosophy: Permissions Are Data, Not Logic

Don't embed permissions in your component tree. Treat permissions as configuration that your components consume. This single shift unlocks everything.

Here's what bad looks like:

// ❌ Don't do this
export function AIFeatureCard() {
  const { user } = useAuth();

  if (user.role !== 'admin' && user.role !== 'premium_subscriber') {
    return null;
  }

  return <div>AI Feature</div>;
}

Enter fullscreen mode Exit fullscreen mode

Problems:

  1. Role names are magic strings
  2. Permission logic is fragmented across 20 components
  3. Adding a new role? Find and update every check
  4. No way to test permissions without rendering components
  5. No audit trail of what permission was checked where

Here's what good looks like:

// ✅ Do this
const canAccessAIFeature = await checkPermission('features:ai:access', {
  userId,
  tenantId,
});

if (canAccessAIFeature) {
  return <AIFeatureCard />;
}

Enter fullscreen mode Exit fullscreen mode

Now permissions are data. You can log them, cache them, test them, audit them.

Type-Safe Permission Definitions

Start with TypeScript. Define every permission your app has as a const object:

// permissions.ts
export const PERMISSIONS = {
  // Organization management
  'org:create': 'Create organization',
  'org:update': 'Update organization settings',
  'org:delete': 'Delete organization',
  'org:invite_members': 'Invite team members',

  // AI features (your feature gates)
  'features:ai:access': 'Access any AI feature',
  'features:ai:documents': 'Use document analysis',
  'features:ai:workflows': 'Create automation workflows',
  'features:ai:exports': 'Export AI-generated content',

  // Admin
  'admin:billing': 'Manage billing',
  'admin:audit_logs': 'View audit logs',
} as const;

// Extract type: 'org:create' | 'org:update' | ...
export type PermissionKey = keyof typeof PERMISSIONS;

Enter fullscreen mode Exit fullscreen mode

This gives you autocomplete and catches typos at compile time. No more permission strings as magic text.

Building the Permission Engine

Your permission engine lives on the backend and is queried from React. Here's the FastAPI service:

# permissions.py
from enum import Enum
from typing import Set
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session

class RoleType(str, Enum):
    OWNER = "owner"
    ADMIN = "admin"
    MEMBER = "member"
    GUEST = "guest"

# Define role -> permissions mapping
ROLE_PERMISSIONS: dict[RoleType, Set[str]] = {
    RoleType.OWNER: {
        "org:create", "org:update", "org:delete", "org:invite_members",
        "features:ai:access", "features:ai:documents", "features:ai:workflows",
        "features:ai:exports", "admin:billing", "admin:audit_logs",
    },
    RoleType.ADMIN: {
        "org:update", "org:invite_members",
        "features:ai:access", "features:ai:documents", "features:ai:workflows",
        "features:ai:exports", "admin:audit_logs",
    },
    RoleType.MEMBER: {
        "features:ai:access", "features:ai:documents", "features:ai:workflows",
    },
    RoleType.GUEST: {
        "features:ai:documents",  # Read-only
    },
}

# Handle permission inheritance for multi-tenant
class PermissionResolver:
    def __init__(self, db: Session):
        self.db = db

    async def get_user_permissions(
        self, user_id: str, tenant_id: str
    ) -> Set[str]:
        """Resolve all permissions for a user in a tenant."""
        # Query user role in this tenant
        membership = self.db.query(TenantMembership).filter(
            TenantMembership.user_id == user_id,
            TenantMembership.tenant_id == tenant_id,
        ).first()

        if not membership:
            return set()

        # Start with their direct role permissions
        permissions = ROLE_PERMISSIONS.get(membership.role, set()).copy()

        # Add custom permissions (if you've granted individual perms)
        custom = self.db.query(UserPermission).filter(
            UserPermission.user_id == user_id,
            UserPermission.tenant_id == tenant_id,
        ).all()

        for perm in custom:
            permissions.add(perm.permission_key)

        return permissions

    async def can_perform(
        self, user_id: str, tenant_id: str, permission: str
    ) -> bool:
        """Check if user can perform an action."""
        permissions = await self.get_user_permissions(user_id, tenant_id)
        return permission in permissions

# Expose as endpoint
@app.post("/api/permissions/check")
async def check_permission(
    request: CheckPermissionRequest,  # {permission, tenant_id}
    user_id: str = Depends(get_current_user_id),
    db: Session = Depends(get_db),
) -> dict[str, bool]:
    resolver = PermissionResolver(db)
    allowed = await resolver.can_perform(
        user_id, request.tenant_id, request.permission
    )
    return {"allowed": allowed}

Enter fullscreen mode Exit fullscreen mode

Notice: No logic in components. All rules live in ROLE_PERMISSIONS and the database. When you add a feature, you update ROLE_PERMISSIONS once. Done.

React Hook: usePermission

Now the React side. Create a hook that queries your permission endpoint:

// usePermission.ts
import { useAuth } from './useAuth';
import { PermissionKey, PERMISSIONS } from './permissions';

interface UsePermissionOptions {
  cacheSeconds?: number;
}

export function usePermission(
  permissionKey: PermissionKey,
  options: UsePermissionOptions = {}
) {
  const { user, tenantId } = useAuth();
  const [allowed, setAllowed] = React.useState<boolean | null>(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState<Error | null>(null);

  React.useEffect(() => {
    if (!user || !tenantId) {
      setLoading(false);
      setAllowed(false);
      return;
    }

    const checkPerm = async () => {
      try {
        const response = await fetch('/api/permissions/check', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            permission: permissionKey,
            tenant_id: tenantId,
          }),
        });

        if (!response.ok) throw new Error('Permission check failed');
        const data = await response.json();
        setAllowed(data.allowed);
      } catch (err) {
        setError(err as Error);
        setAllowed(false); // Fail closed
      } finally {
        setLoading(false);
      }
    };

    checkPerm();
  }, [user?.id, tenantId, permissionKey]);

  return { allowed, loading, error };
}

Enter fullscreen mode Exit fullscreen mode

Usage in components:

// AIFeatureCard.tsx
export function AIFeatureCard() {
  const { allowed, loading } = usePermission('features:ai:access');

  if (loading) return <Skeleton />;
  if (!allowed) return null;

  return <div className="p-4 bg-blue-50">AI Feature</div>;
}

Enter fullscreen mode Exit fullscreen mode

Clean. Testable. Type-safe.

Caching: The Performance Multiplier

This burned me: I shipped this without caching, and every component requesting the same permission hammered the backend. Use React Query:

// usePermission.ts (improved)
import { useQuery } from '@tanstack/react-query';

export function usePermission(
  permissionKey: PermissionKey,
  options: UsePermissionOptions = {}
) {
  const { user, tenantId } = useAuth();
  const cacheSeconds = options.cacheSeconds ?? 300; // 5 min default

  return useQuery({
    queryKey: ['permission', tenantId, permissionKey],
    queryFn: async () => {
      const response = await fetch('/api/permissions/check', {
        method: 'POST',
        body: JSON.stringify({
          permission: permissionKey,
          tenant_id: tenantId,
        }),
      });
      const data = await response.json();
      return data.allowed;
    },
    staleTime: cacheSeconds * 1000,
    enabled: !!user && !!tenantId,
  });
}

Enter fullscreen mode Exit fullscreen mode

Now the second component asking for features:ai:access hits the cache, not your backend.

Gotcha: Stale Permission State After Tenant Changes