Learning lens
Start with judgment, not a universal prescription.
This page is a learning model for understanding the capability. It does not claim exact production internals, exact costs, or an infinite-scale architecture.
Evidence
Conceptual pattern
0->1 default
Start with clear tenant boundaries, boring data ownership, and reversible choices before optimizing for platform scale.
Cost drivers
Watch for breakpoints
- -large tenants affect small tenants
- -support teams need self-serve controls
- -migration work becomes tenant-specific
Learn first
Concepts behind this architecture
JWT
A signed JSON token that carries claims between systems.
OAuth2 Authorization Code Flow
Let users grant access without giving your app their password.
Strong Consistency
Make reads reflect the latest committed write for correctness-critical data.
TLS
Encrypt and authenticate network connections between clients and services.
The Problem
As applications grow beyond a single user, you need to control who can do what. A junior developer shouldn’t delete production databases. A team member shouldn’t see billing details. A viewer shouldn’t edit documents. Role-Based Access Control (RBAC) assigns permissions to roles, and roles to users — but production systems need more: hierarchical roles, resource-level permissions, attribute-based policies, and real-time permission changes. The authorization check must be fast (sub-millisecond), consistent, and auditable, sitting on every request’s critical path.
Architectural Approaches
Simple RBAC (Role → Permissions)
Define a fixed set of roles (admin, editor, viewer), each with a set of permissions (create, read, update, delete). Assign roles to users. On each request, check if the user’s role includes the required permission. Implemented with a roles table and a role_permissions mapping.
Pros
- +Simple to understand, implement, and audit
- +Works well for applications with clear role hierarchies
- +Fast evaluation — single lookup per request
- +Easy to explain permission model to end users
Cons
- −Role explosion — too many roles for fine-grained needs
- −No resource-level permissions (can edit ALL documents, not specific ones)
- −Adding a new permission requires updating role definitions
- −Doesn't handle complex conditions (time-based, attribute-based)
Relationship-Based Access Control (ReBAC)
Permissions are derived from relationships between users and resources. “User X can edit Document Y because X is a member of Team Z which owns Folder W which contains Document Y.” Models the permission graph as a series of relationships (tuples). Used by Google Zanzibar and its open-source derivatives (SpiceDB, OpenFGA).
Pros
- +Naturally handles resource-level permissions
- +Supports complex hierarchies (org → team → project → document)
- +Permission inheritance through relationship chains
- +Scales to billions of relationships with proper infrastructure
Cons
- −More complex to implement and reason about
- −Relationship graph traversal can be slow without caching
- −Debugging "why can/can't user X do Y?" is harder
- −Requires a dedicated authorization service or infrastructure
Attribute-Based Access Control (ABAC)
Permissions are evaluated based on attributes of the user, resource, action, and environment. Policies are written as rules: “Allow if user.department == resource.department AND action == ‘read’ AND time.hour >= 9 AND time.hour <= 17.” Most flexible but most complex.
Pros
- +Extremely flexible — any attribute can be a policy input
- +No role explosion — policies adapt to any dimension
- +Supports contextual conditions (time, location, IP)
- +Can model RBAC as a subset (role is just an attribute)
Cons
- −Policy complexity can become unmanageable
- −Slower evaluation — must fetch attributes at decision time
- −Harder to audit ("who has access to what?" is a graph problem)
- −Requires a policy language (Rego, Cedar, Casbin)
Architecture
Key Components
Policy Engine
Evaluates authorization policies against the request context (user, resource, action). Supports RBAC role checks, ReBAC relationship traversal, or ABAC attribute evaluation depending on the chosen model. Must return a decision (allow/deny) in under 1ms.
Role & Permission Store
Database tables storing roles, permissions, role-permission mappings, and user-role assignments. In ReBAC, stores relationship tuples (user, relation, object). Supports tenant-scoped roles for multi-tenant applications.
Permission Cache
Redis-based cache of evaluated permissions. Caches user→role mappings and frequently checked permissions. Invalidated on role changes. TTL-based expiry ensures eventual consistency (typically 5-60 second TTL).
Authorization Middleware
Intercepts every API request, extracts the user identity from the JWT, determines the required permission for the endpoint, and calls the policy engine. Returns 403 Forbidden if the check fails. Must be applied consistently across all routes.
Admin Dashboard
UI for managing roles, permissions, and assignments. Supports creating custom roles, viewing effective permissions for a user, and simulating permission checks (“what would happen if user X tried action Y on resource Z?”).
Audit Logger
Records every authorization decision (who, what, when, allowed/denied) for compliance and debugging. Enables answering “who accessed this resource?” and “why was this user denied access?” questions.
Data Model
Scoped Roles and Resource-Level Permissions
Basic RBAC assigns global roles — “User X is an admin.” But most apps need scoped roles — “User X is an admin OF project Y but only a viewer OF project Z.”
Implementation: Add scope fields to the user-role assignment:
scope_type: “organization”, “project”, “team”, “document”scope_id: the specific resource ID
Permission check flow:
- User requests to edit Project Y
- Look up user’s roles scoped to Project Y
- Also look up roles scoped to Project Y’s parent (Organization Z) — role inheritance
- Check if any of those roles include the “project:edit” permission
- Allow or deny
Hierarchy: Permissions inherit downward. An org admin is implicitly a project admin for all projects in the org. Implement by walking up the resource hierarchy during permission checks. Cache the effective permissions per (user, scope) pair.
Performance: With scoped roles, the permission check involves multiple lookups. Pre-compute and cache the effective permission set per user-scope combination. Invalidate cache on role changes (pub/sub notification from the role service).
Google Zanzibar and ReBAC
Google’s Zanzibar paper (2019) introduced a scalable authorization system based on relationship tuples.
Core concept: Authorization data is stored as tuples: (object, relation, user). For example:
(doc:readme, owner, user:alice)— Alice owns the readme doc(folder:eng, viewer, team:engineering#member)— Engineering team members can view the eng folder(doc:readme, parent, folder:eng)— readme doc is in the eng folder
Permission check: “Can Alice view doc:readme?” The system checks:
- Is Alice a direct viewer of doc:readme? → No
- Does doc:readme have a parent? → Yes, folder:eng
- Is Alice a viewer of folder:eng? → Check if Alice is a member of team:engineering → Yes!
- Permission granted via relationship chain.
Open-source implementations:
- SpiceDB (AuthZed) — Go-based, gRPC API, closest to Zanzibar
- OpenFGA (Auth0/Okta) — supports multiple storage backends
- Permify — Kubernetes-native, schema-first approach
ReBAC is ideal when your permission model mirrors your data hierarchy (organizations → projects → resources).
Performance and Caching Strategy
Authorization sits on the hot path of every request. It must be fast.
Caching layers:
- In-process cache (LRU, 1000 entries, 30s TTL): Fastest. Caches (user_id, permission, scope) → boolean. Hit rate: 60-80% for typical web apps.
- Redis cache (60s TTL): Shared across instances. Caches user’s effective role set per scope. Hit rate: 90%+.
- Database (source of truth): Only hit on cache miss. Must be fast — proper indexes on user_id, role_id, scope columns.
Cache invalidation: When a role assignment changes:
- Invalidate the user’s cache entries (in-process + Redis)
- Publish an event so all instances invalidate their local cache
- For security-critical changes (role revocation), force re-evaluation by bumping a version counter
Batch prefetching: For UI pages that show multiple resources, prefetch permissions for all visible resources in one batch call. “Can user X do [view, edit, delete] on resources [A, B, C, D]?” — single round-trip returning a permission matrix.
Latency targets: In-process cache hit: <0.01ms. Redis cache hit: <1ms. Database lookup: <5ms. Any authorization check >10ms indicates a problem.
Real-World Examples
Google Workspace
Uses Zanzibar for authorization across Drive, Docs, Calendar, and more. Handles trillions of authorization checks per day. Relationship tuples model sharing (viewer, editor, owner) with inheritance through folders and organizational units.
GitHub
Scoped RBAC with organization → team → repository hierarchy. Roles (admin, maintain, write, triage, read) are scoped per repository. Team-level permissions inherit to repositories. Custom repository roles available for Enterprise.
AWS IAM
ABAC policy engine evaluating JSON policies against request context. Policies attached to users, groups, and roles. Supports conditions (IP range, time, MFA), resource-level permissions, and cross-account access via trust policies.
Notion
ReBAC model where permissions flow through the page hierarchy (workspace → teamspace → page → subpage). Supports sharing individual pages with specific users or groups, overriding inherited permissions.
Architectural Tradeoffs
RBAC vs ReBAC vs ABAC
Pros
- +RBAC: simplest to implement and reason about
- +ReBAC: natural for hierarchical data, resource-level permissions
- +ABAC: most flexible, handles any authorization pattern
Cons
- −RBAC: role explosion for fine-grained needs
- −ReBAC: requires dedicated infrastructure (SpiceDB, OpenFGA)
- −ABAC: policy complexity can become unmanageable
Embedded authorization vs external service
Pros
- +Embedded: lowest latency, simplest deployment
- +External: centralized policy management across microservices
- +Embedded: no additional infrastructure dependency
Cons
- −Embedded: duplicated logic across services, harder to audit
- −External: adds network hop, becomes a critical dependency
- −External: must handle service unavailability (fail open vs closed)
Fail-open vs fail-closed on authorization service outage
Pros
- +Fail-open: application stays available during auth service outage
- +Fail-closed: no unauthorized access is ever possible
- +Cached decisions provide a middle ground
Cons
- −Fail-open: security risk during outages
- −Fail-closed: application becomes unavailable if auth service is down
- −Cached decisions may be stale (revoked permissions still cached)
Learn Role-Based Access Control
Copy a learning context prompt for an AI assistant. It keeps assumptions visible and asks for tradeoffs, cost drivers, failure modes, and what to measure before adding complexity.
Explore More Capabilities
API Versioning
Backward-compatible API evolution with URL, header, and content negotiation strategies
Audit Logging
Append-only event stores, tamper-proof records, and compliance-ready activity trails
Caching Strategies
Cache-aside, write-through, invalidation patterns, and distributed caching with Redis