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

Capstone: Multi-Tenant Notes API

What we built

Starting from a simple user-owned notes app, we built a complete multi-tenant authorization system:

LayerWhat it doesWhere it lives
RolesOwner, editor, viewer per orgmemberships table
Role hierarchyOwner > editor > viewerauthorize.ts
PermissionsFine-grained resource:action stringspermissions.ts
Custom rolesOrg-specific roles with custom permission setscustom_roles table
Org scopingEvery query includes org_idEvery db.prepare call
Org switchingActive org in sessionsessions.ts
InvitesEmail-based, role-limited, single-useinvites table
API keysHashed, org-bound, scopedapi_keys table
PolicyCentralized can(user, action, resource)policy.ts
AuditEvery decision loggedlogger.ts

Project structure

src/
  app.ts                 # setup(), routes
  server.ts              # starts the server
  db.ts                  # SQLite schema, seed data
  auth.ts                # authenticate (session + API key)
  sessions.ts            # session store with activeOrgId
  cookies.ts             # cookie helpers
  membership.ts          # getMembership, getUserOrgs
  permissions.ts         # ROLE_PERMISSIONS, getPermissions, roleHasPermission
  authorize.ts           # requireRole, requirePermission
  policy.ts              # can(), authorize()
  api-keys.ts            # createApiKey, validateApiKey
  logger.ts              # structured logging
  routes/
    auth.ts              # POST /login
    orgs.ts              # /me/orgs, /orgs/:orgId/activate, invites, roles, api-keys
    notes.ts             # CRUD scoped to org

The authorization flow

For any request, the authorization flow is:

Request arrives
│
├─ authenticate(request)
│   ├─ API key? → validate hash, get userId + orgId + scopes
│   └─ Session cookie? → get userId + activeOrgId
│
├─ authorize({ user, orgId, resourceOwnerId }, action)
│   ├─ getMembership(userId, orgId) → role
│   ├─ getPermissions(role, orgId) → permission set
│   ├─ permission set includes action? → check
│   ├─ API key scopes include action? → check
│   ├─ Special rules (creator edit, owner bypass)? → check
│   └─ Log the decision (allowed or denied)
│
└─ Handler executes (or returns 403/404)

Every step is a plain function. No framework, no middleware, no magic. The functions compose naturally: authenticate returns a user, the policy checks the user’s access, the handler does the work.

Test the full system

# === Setup ===
npm run dev

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

# Activate Acme
curl -b cookies.txt -X POST http://localhost:3000/me/orgs/org-acme/activate

# === RBAC ===
# Alice can list, create, and delete notes (owner)
curl -b cookies.txt http://localhost:3000/orgs/org-acme/notes
curl -b cookies.txt -X POST http://localhost:3000/orgs/org-acme/notes \
  -H "Content-Type: application/json" \
  -d '{"title":"Owner Note","body":"Created by Alice"}'

# Log in as Carol (viewer) — can list but not create
curl -c cookies.txt -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"password123"}'
curl -b cookies.txt http://localhost:3000/orgs/org-acme/notes       # 200
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"}'          # 403

# === Org scoping ===
# Bob cannot see Globex notes (not a member)
curl -c cookies.txt -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"password123"}'
curl -b cookies.txt http://localhost:3000/orgs/org-globex/notes      # 404

# === API keys ===
# Alice creates a read-only key for Acme
curl -c cookies.txt -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"password123"}'
curl -b cookies.txt -X POST http://localhost:3000/orgs/org-acme/api-keys \
  -H "Content-Type: application/json" \
  -d '{"name":"CI Read","scopes":["notes:read"]}'
# Copy the key from the response

# Use the key — read works, create fails
curl -H "X-API-Key: sk_..." http://localhost:3000/orgs/org-acme/notes          # 200
curl -H "X-API-Key: sk_..." -X POST http://localhost:3000/orgs/org-acme/notes \
  -H "Content-Type: application/json" \
  -d '{"title":"Key Create","body":"Should fail"}'                              # 403

What you understand now

You can now build authorization systems for real apps. The concepts transfer directly:

  • SaaS apps: Organizations are workspaces or teams. Roles are admin, member, guest. Permissions control feature access.
  • Content platforms: Organizations are channels or publications. Roles are editor, author, subscriber. Permissions control publish, draft, comment.
  • Developer tools: Organizations are projects. API keys have scopes. Audit logs track deployments and configuration changes.

The specific roles, permissions, and resources change. The patterns are the same: authenticate, resolve the context, check the policy, log the decision.

Challenges

Challenge 1: Add resource-level permissions. Instead of “editors can edit all notes,” implement “editors can only edit notes they created.” This is partly done in the policy function’s special case. Extend it to be configurable per-organization.

Challenge 2: Add a role management UI. Build routes for: listing members of an org, changing a member’s role, and removing a member. Apply proper authorization (only owners can change roles, owners cannot remove themselves if they are the last owner).

Challenge 3: Add time-limited API keys. Add an expires_at column to the api_keys table. Reject expired keys during validation. Add a GET /orgs/:orgId/api-keys route that lists active keys (showing prefix and scopes, not the key itself).

Challenge 4: Implement ABAC. Add a rule: “Notes tagged with ‘confidential’ can only be viewed by owners.” This requires checking a resource attribute (the tag) in the policy function, not just the user’s role.

What pattern does every authorization check in this course follow?

Which authorization layer provides the finest-grained control?

← Authorization Checklist Back to course →

© 2026 hectoday. All rights reserved.