Skip to content
Authorization40-55 min

Role-Based Access Control Implementation

This guide implements RBAC with centralized permissions, backend guards, frontend guards, database checks, and a simple verification checklist.

Node.jsReactTypeScript

Prerequisites

  • An authenticated user object available on API requests.
  • A role field on users or memberships.
  • A list of actions each role should be allowed to perform.
  • Protected backend routes that need authorization.
1

Model roles and permissions

Start simple. Most products need roles like owner, admin, member, and viewer before they need a complex permission editor.

Implementation snippet
export const permissions = {
  owner: ["team:read", "team:update", "billing:manage", "users:invite", "users:remove"],
  admin: ["team:read", "team:update", "users:invite"],
  member: ["team:read"],
  viewer: ["team:read"],
} as const;

export type Role = keyof typeof permissions;
export type Permission = (typeof permissions)[Role][number];
2

Create a permission helper

Implementation snippet
export function can(role: Role, permission: Permission) {
  return permissions[role]?.includes(permission) ?? false;
}
3

Protect backend routes

  1. Run authentication middleware first.
  2. Load the user's role for the relevant workspace, organization, or account.
  3. Check the required permission before the route handler.
  4. Return 403 when the user is authenticated but not allowed.
Implementation snippet
export function requirePermission(permission: Permission) {
  return async (req, res, next) => {
    const membership = await memberships.findByUserAndTeam(req.user.id, req.params.teamId);
    if (!membership || !can(membership.role, permission)) {
      return res.status(403).json({ message: "You do not have access to this action" });
    }

    req.membership = membership;
    next();
  };
}

app.post("/teams/:teamId/invites", requireAuth, requirePermission("users:invite"), createInvite);
4

Hide unavailable UI actions

Frontend checks improve usability, but they do not replace backend authorization.

Implementation snippet
function InviteButton({ role }: { role: Role }) {
  if (!can(role, "users:invite")) return null;
  return <button>Invite teammate</button>;
}
5

Store roles safely

  1. Store role at the membership level for team-based products.
  2. Allow one owner or multiple owners depending on your recovery policy.
  3. Prevent users from changing their own role to a higher privilege.
  4. Audit role changes with who changed what and when.
6

Verification checklist

Checklist
  • A viewer cannot call admin-only API routes directly.
  • A member cannot invite users unless the permission list allows it.
  • An admin cannot access billing if only owner has `billing:manage`.
  • UI hides unavailable actions but backend still rejects forged requests.
  • Role changes take effect after token/session refresh or next API role lookup.

Need implementation help?

Want this built correctly in your codebase?

Send us your stack, repo context, and the feature you need. We will help you implement it cleanly and hand over the working code.

Free scoping callFixed timelineFull source ownership
Get implementation help