hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Authorization with @hectoday/http

Beyond Authentication

  • Authentication vs. Authorization
  • Project Setup

Role-Based Access Control (RBAC)

  • Roles and What They Mean
  • Checking Roles in Route Handlers
  • Role Hierarchy

Permission-Based Access Control

  • From Roles to Permissions
  • Checking Permissions
  • Custom Permissions

Organization Scoping

  • Multi-Tenancy
  • Switching Organizations
  • Inviting Members

API Keys and Scoping

  • API Keys
  • Scoped API Keys

Putting It All Together

  • Policy Functions
  • Audit Logging
  • Authorization Checklist
  • Capstone: Multi-Tenant Notes API

Role Hierarchy

The problem from the previous lesson

requireRole(user, orgId, "viewer") checks for an exact match. An owner is not a viewer — the check fails. But conceptually, an owner can do everything a viewer can do, and more.

We need a hierarchy: owner > editor > viewer. Checking “at least viewer” passes for owners, editors, and viewers. Checking “at least editor” passes for owners and editors. Checking “at least owner” passes only for owners.

Define the hierarchy

Update src/authorize.ts:

// src/authorize.ts
import { getMembership } from "./membership.js";
import type { AuthUser } from "./auth.js";

const ROLE_LEVELS: Record<string, number> = {
  viewer: 1,
  editor: 2,
  owner: 3,
};

export function requireRole(user: AuthUser, orgId: string, minimumRole: string): true | Response {
  const membership = getMembership(user.id, orgId);

  if (!membership) {
    return Response.json({ error: "Not found" }, { status: 404 });
  }

  const userLevel = ROLE_LEVELS[membership.role] ?? 0;
  const requiredLevel = ROLE_LEVELS[minimumRole] ?? 0;

  if (userLevel < requiredLevel) {
    return Response.json({ error: "Forbidden" }, { status: 403 });
  }

  return true;
}

Each role has a numeric level. requireRole checks if the user’s level is at least the required level. An owner (level 3) passes any check. An editor (level 2) passes “at least viewer” and “at least editor” but not “at least owner.” A viewer (level 1) only passes “at least viewer.”

The ROLE_LEVELS object is the hierarchy definition. Adding a new role means adding one line (like moderator: 2 to place it at the same level as editor).

No code changes needed in routes

The route handlers from the previous lesson do not change. They already use requireRole:

// View: any member (at least viewer)
requireRole(user, orgId, "viewer");

// Create: editors and above
requireRole(user, orgId, "editor");

// Delete: owners only
requireRole(user, orgId, "owner");

The meaning changed (from exact match to minimum level), but the call sites stay the same. This is a good sign — the abstraction is clean.

Try it

# Log in as Alice (owner at Acme)
curl -c cookies.txt -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"password123"}'

# List notes — now works (owner >= viewer)
curl -b cookies.txt http://localhost:3000/orgs/org-acme/notes

# Create a note — works (owner >= editor)
curl -b cookies.txt -X POST http://localhost:3000/orgs/org-acme/notes \
  -H "Content-Type: application/json" \
  -d '{"title":"New Note","body":"Created by Alice"}'

# Log in as Carol (viewer at Acme)
curl -c cookies.txt -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"password123"}'

# List notes — works (viewer >= viewer)
curl -b cookies.txt http://localhost:3000/orgs/org-acme/notes

# Create a note — 403 (viewer < editor)
curl -b cookies.txt -X POST http://localhost:3000/orgs/org-acme/notes \
  -H "Content-Type: application/json" \
  -d '{"title":"Should Fail","body":"Carol cannot create"}'

When hierarchy is not enough

Role hierarchy works well when permissions are strictly layered: every higher role can do everything a lower role can do. But what if you want an editor who can edit notes but cannot manage members? With a hierarchy, if member management requires “at least editor,” then all editors can manage members.

The next section introduces permissions, which decouple specific actions from the role hierarchy. An editor might have notes:create and notes:edit but not members:invite.

Exercises

Exercise 1: Test all three users (Alice, Bob, Carol) against all three operations (list, create, delete) in Acme. Verify the hierarchy works: Alice can do everything, Bob can list and create, Carol can only list.

Exercise 2: Log in as Alice and try to access Globex notes. She is a viewer there. She should be able to list but not create or delete.

Exercise 3: What happens if a membership has a role not in ROLE_LEVELS (e.g., "guest")? The ?? 0 fallback gives it level 0, which is below viewer. The user cannot do anything. This is a safe default.

Why do we use numeric levels instead of checking the role string directly?

← Checking Roles in Route Handlers From Roles to Permissions →

© 2026 hectoday. All rights reserved.