Auth & Security/
Lesson

Every multi-user application eventually needs to answer the question: "Which users can do which things?" The simplest answer is RBACWhat is rbac?Role-Based Access Control - assigning permissions to roles (like admin or editor), then giving users roles instead of individual permissions., you define roles, attach permissions to those roles, and assign roles to users. Instead of managing permissions for each user individually, you manage a handful of roles.

Nearly every SaaS product you have used has RBAC under the hood. GitHub has Owner, Admin, Write, Triage, and Read roles. Slack has Workspace Owner, Admin, and Member. Google Workspace has Super Admin, Groups Admin, User Management Admin, and so on. The pattern is everywhere because it works.

The RBACWhat is rbac?Role-Based Access Control - assigning permissions to roles (like admin or editor), then giving users roles instead of individual permissions. model

RBAC has three core concepts that chain together:

Users → Roles → Permissions

A user is assigned one or more roles. Each role grants a set of permissions. When the system checks whether a user can perform an action, it looks up their roles and checks whether any role includes the required permission.

ConceptWhat it representsExample
UserAn individual account[email protected]
RoleA named collection of permissions"editor"
PermissionA specific action on a specific resource"posts:update"

Here is a concrete mapping for a content management system:

RolePermissions
viewerposts:read, comments:read
editorposts:read, posts:create, posts:update, comments:read, comments:create
adminposts:read, posts:create, posts:update, posts:delete, comments:read, comments:create, comments:delete, users:manage
02

Role hierarchies

In many systems, roles form a hierarchy where higher roles inherit all permissions from lower roles. An admin can do everything an editor can do, plus more. An editor can do everything a viewer can do, plus more.

admin
  └── editor
        └── viewer

This simplifies permission management. When you add a new "read" permission, you add it to the viewer role and it automatically applies to editors and admins. Without hierarchy, you would need to update every role individually.

AI pitfall
AI almost never implements role hierarchies. It generates flat role checks like if (role === 'admin' || role === 'editor') for every route. As your role list grows, these checks become brittle and error-prone. One missed role in one route handler creates a permissions gap.
03

Database schemaWhat is schema?A formal definition of the structure your data must follow - which fields exist, what types they have, and which are required. for RBACWhat is rbac?Role-Based Access Control - assigning permissions to roles (like admin or editor), then giving users roles instead of individual permissions.

A production RBAC system needs at minimum these tables:

-- Users table
CREATE TABLE users (
  id TEXT PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL
);

-- Roles table
CREATE TABLE roles (
  id TEXT PRIMARY KEY,
  name TEXT UNIQUE NOT NULL,
  description TEXT
);

-- User-role mapping (many-to-many)
CREATE TABLE user_roles (
  user_id TEXT REFERENCES users(id),
  role_id TEXT REFERENCES roles(id),
  PRIMARY KEY (user_id, role_id)
);

-- Permissions table
CREATE TABLE permissions (
  id TEXT PRIMARY KEY,
  action TEXT NOT NULL,       -- e.g., 'create', 'read', 'update', 'delete'
  resource TEXT NOT NULL,     -- e.g., 'posts', 'comments', 'users'
  UNIQUE(action, resource)
);

-- Role-permission mapping (many-to-many)
CREATE TABLE role_permissions (
  role_id TEXT REFERENCES roles(id),
  permission_id TEXT REFERENCES permissions(id),
  PRIMARY KEY (role_id, permission_id)
);

This schema lets you assign multiple roles to a user and multiple permissions to a role. Changes to a role's permissions immediately affect every user with that role.

Compare this to what AI typically generates:

-- AI-generated: role stored as a string on the user record
CREATE TABLE users (
  id TEXT PRIMARY KEY,
  email TEXT,
  role TEXT DEFAULT 'viewer'  -- Single role, no flexibility
);

The AI version stores a single role as a string directly on the user record. This means a user can only have one role, there is no formal permissions table, and changing what a role can do requires updating application code instead of database records.

04

MiddlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. patterns for RBACWhat is rbac?Role-Based Access Control - assigning permissions to roles (like admin or editor), then giving users roles instead of individual permissions.

The right way to enforce RBAC is through middleware that runs before your route handlers. The middleware looks up the user's roles and permissions and either allows the request to proceed or returns a 403.

// Permission check middleware
function requirePermission(resource, action) {
  return async (req, res, next) => {
    const userPermissions = await getUserPermissions(req.user.id);
    const required = `${resource}:${action}`;

    if (!userPermissions.includes(required)) {
      return res.status(403).json({
        error: `Missing permission: ${required}`
      });
    }
    next();
  };
}

// Usage: clean, declarative route definitions
app.get('/api/posts', authenticate, requirePermission('posts', 'read'), listPosts);
app.post('/api/posts', authenticate, requirePermission('posts', 'create'), createPost);
app.delete('/api/posts/:id', authenticate, requirePermission('posts', 'delete'), deletePost);

Reading the route definitions tells you exactly what permissions are required. You do not need to read the handler code to understand the access control.

05

How AI oversimplifies role checks

AI generates role checks as inline string comparisons scattered throughout route handlers:

// AI-generated: role checks scattered everywhere
app.delete('/api/posts/:id', authenticate, async (req, res) => {
  if (req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Admin only' });
  }
  // ... delete logic
});

app.put('/api/posts/:id', authenticate, async (req, res) => {
  if (req.user.role !== 'admin' && req.user.role !== 'editor') {
    return res.status(403).json({ error: 'Not allowed' });
  }
  // ... update logic
});

app.get('/api/users', authenticate, async (req, res) => {
  if (req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Admin only' });
  }
  // ... list users
});

Three problems with this pattern. First, role strings are hardcoded, if you rename "editor" to "contributor," you must find and update every check. Second, adding a new role means modifying every route that should include it. Third, there is no single place to audit all permission rules. You have to read every route handlerWhat is route handler?A Next.js file named route.js inside the app/ directory that handles HTTP requests directly - the App Router equivalent of API routes. to understand the full access control picture.

AI pitfall
When you ask AI to "add admin-only access to this route," it adds if (req.user.role !== 'admin') inside the handler. When you then ask it to "also allow editors," it adds || req.user.role !== 'editor', and sometimes gets the boolean logic wrong, accidentally locking out everyone or letting everyone in.
06

Centralized permission checking

A centralized permission function is easier to audit, test, and maintain:

// Single source of truth for permissions
const ROLE_PERMISSIONS = {
  admin:  ['posts:read', 'posts:create', 'posts:update', 'posts:delete',
           'users:read', 'users:manage'],
  editor: ['posts:read', 'posts:create', 'posts:update'],
  viewer: ['posts:read'],
};

function hasPermission(user, permission) {
  const roles = user.roles || [user.role];
  return roles.some(role =>
    ROLE_PERMISSIONS[role]?.includes(permission)
  );
}

Now you can read the entire permission model in one place. Adding a role or changing permissions is a single edit. Testing is straightforward, you pass different user objects and check the return value.

07

Common RBACWhat is rbac?Role-Based Access Control - assigning permissions to roles (like admin or editor), then giving users roles instead of individual permissions. mistakes to watch for

MistakeWhat goes wrongHow to fix it
Single role per userA user who is both "editor" and "billing-admin" cannot be representedUse a many-to-many user-role relationship
No default roleNew users have undefined permissionsAssign a default role (e.g., "viewer") at registration
Checking roles instead of permissionsCode checks role === 'admin' instead of hasPermission('users:delete')Check permissions, not roles, roles are just containers
No deny-by-defaultMissing middleware means the route is openApply a global auth middleware, then add permission checks per route
Hardcoded role stringsTypos create silent permission failuresUse constants or enums for role and permission names