Building your own authorization system seems straightforward at first. You need to check if users can access resources, so you write some code, add a few database columns, and move on. This approach works for monolithic applications with simple use cases, but it falls apart as your application scales or if you add more applications and services that need to use the same authorization logic. The problems that emerge are predictable, expensive to fix, and often force teams into a corner where they can't deliver the features their business needs.
The database bottleneck
Custom authorization implementations typically store permission data in the application's main database. Every authorization check requires fetching data from this database, which means your source of truth gets hammered with permission queries on every request. This creates immediate performance problems.
The AuthZed founders first started a company called Quay, the first private docker registry. The founders kept running into authorization challenges that impacted adding more features into Quay, the product. When developers at Quay analyzed their authorization queries, they discovered joins across 11 tables consuming substantial database CPU. The authorization logic had grown complex enough that these queries competed with actual application data access for database resources.
Beyond performance, this coupling makes scaling difficult. If your application needs to expand into new geographic regions, your authorization system must follow. If your database doesn't support global distribution, you'll need to refactor your entire authorization layer to handle multi-region deployments.
The inflexibility trap
Initial authorization systems start simple because the requirements seem simple. You might implement basic role checks or resource ownership. Then business requirements change.
Enterprise customers request user-defined roles so their admins can create custom permissions. Product teams need fine-grained authorization to control access down to individual document pages or file sections. Organizations want recursive relationships where teams can contain teams, each with different permission hierarchies. These features require fundamental changes to your authorization model.
Without proper architecture, each new requirement forces you to refactor security-critical code. Teams often cancel features because the authorization system can't support them. Going back to the Quay example, recursive namespace features remained on the backlog indefinitely because the MySQL-based authorization system couldn't execute recursive queries to traverse team hierarchies.
Microservices create authorization silos
When authorization logic lives embedded in application code, moving to a microservices architecture creates painful trade-offs. If the authorization code stays in a monolithic service, other microservices must call back to the monolith for permission checks. This inverts the entire point of service decomposition.
The alternative is extracting authorization logic into shared libraries. This creates new problems:
- Language proliferation: Each programming language your organization uses needs its own implementation of the authorization library. A polyglot shop using Node.js, Python, Java, and Ruby needs four separate implementations.
- Coordination overhead: Updates to authorization logic require synchronized rollouts across all services to prevent inconsistent permission decisions.
- Drift potential: Different implementations may interpret the same authorization rules differently, creating security vulnerabilities.
As teams build additional products, they often create separate authorization implementations rather than fighting with a system that doesn't support their needs. This fragments the user experience. A user might have viewer access in one product and editor access in another product, with no coherent way to understand or manage their permissions across the platform.
The JWT scalability wall
JSON Web Tokens (JWTs) present an attractive option for authorization because they're self-contained and don't require database lookups. But this strength becomes a weakness as permission requirements evolve.
JWTs can't be revoked before their expiration time. When a user's permissions change, the JWT continues to grant the old permissions until it expires. This forces short expiration times, which creates poor user experiences with frequent re-authentication.
Modern applications need fine-grained authorization down to individual resources. A user might have access to thousands, millions, or billions of objects. Fitting these permissions into a JWT passed between services becomes impossible. The token size explodes, network overhead increases, and the architecture breaks down.
Security vulnerabilities from custom code
Authorization code is security-critical. Mistakes grant unauthorized access to sensitive data. Yet authorization logic often becomes tangled with application logic, making it difficult to audit and test thoroughly.
Developers typically don't enjoy working on authorization code. It's complex, high-risk, and not directly related to product features. This creates a culture where engineers avoid touching the authorization system unless absolutely necessary, letting technical debt accumulate.
When authorization logic is embedded throughout the codebase, security reviews must examine every service. There's no single place to understand what permissions exist or how they're enforced. OWASP consistently identifies broken access control as a top security concern, and custom authorization implementations are a primary contributor.
Common mistakes in initial implementations
Several patterns appear repeatedly in custom authorization systems:
Over-reliance on boolean flags
Authorization starts with simple checks: is this user an admin? Does this user own this resource? As requirements grow, developers add more boolean columns to database tables. These flags proliferate and create complicated conditional logic that's hard to maintain or extend.
Missing abstraction layers
Authorization logic gets written directly into route handlers and API endpoints. There's no clean separation between "what action is being attempted" and "how we determine if this action is allowed." Changes to authorization rules require hunting through the entire codebase.
Insufficient caching strategies
Every permission check queries the database. Applications don't implement caching because it's complex to invalidate cached permissions when relationships change. The result is excessive database load.
No audit trail
Custom systems rarely log authorization decisions comprehensively. When investigating security incidents or debugging permission issues, teams lack the data to understand what access was granted and why.
Policy buried in code
Authorization rules exist only as code logic. There's no declarative way to view or update policies. Product managers and security teams can't review permissions without reading through implementation details.
The migration problem
Once a custom authorization system is embedded deeply in an application, replacing it becomes a massive project. Permission data is scattered across database tables. Authorization checks are woven throughout the codebase. Services depend on specific authorization APIs.
Teams often recognize they need a better solution but can't justify the engineering time for a migration. They continue patching the existing system, each patch adding more complexity and technical debt. The system becomes too broken to keep and too entrenched to replace.
This paralysis prevents companies from building features their business needs. If the authorization system can't support a requirement, that requirement gets dropped, delayed, or implemented with workarounds that create new problems.
The unseen opportunity cost
The most expensive pitfall is invisible. Engineers spend time maintaining and extending custom authorization systems instead of building product features. Based on conversations with companies that have moved away from custom implementations, thousands of engineering hours go into authorization infrastructure that doesn't differentiate the product or go into growing the product to support more users to drive more revenue.
Companies that treat authorization as a solved problem, like databases or authentication, can redirect that engineering effort toward their actual value proposition. The authorization system becomes infrastructure that enables business logic rather than an ongoing project that consumes engineering resources.
Recognizing when to seek alternatives
Not every application needs a sophisticated authorization system immediately. An MVP serving initial users might work fine with simple ownership checks and basic roles. The inflection point comes when:
- Enterprise customers request advanced permission features
- Multiple products need consistent authorization models
- Geographic expansion requires distributed permission data
- Engineering time on authorization infrastructure exceeds time on features
- Security incidents stem from authorization complexity
- Requested features get blocked by authorization limitations
At that point, treating authorization as infrastructure rather than application code becomes the practical choice. Centralized authorization systems built on proven patterns can handle requirements that custom implementations struggle with: Google's Zanzibar demonstrates this architecture at global scale, and implementations like SpiceDB make similar capabilities accessible to organizations of any size.
FAQ
Q: When should I build my own authorization system?
A: For MVPs or applications with very simple permission requirements, custom authorization can work but will slow you down as you grow and scale your application. A better option is to use open source SpiceDB for greenfield apps so that permissions can easily evolve as the app develops. Once you need features like user-defined roles, recursive relationships, or consistent permissions across multiple products, a centralized permission system becomes essential.
Q: Can't I just use JWTs for authorization?
A: JWTs work for authentication and basic authorization, but they have fundamental limitations. They can't be revoked before expiration, can't scale to fine-grained permissions on many resources, and don't support dynamic permission changes. See JWT authorization pitfalls for details.
Q: How do I know if my authorization system is becoming a problem?
A: Warning signs include: feature requests blocked by authorization limitations, significant database load from permission queries, difficulty adding new permission types, authorization bugs causing security incidents, and engineering time on authorization maintenance exceeding time on product features.
Q: What's the difference between authentication and authorization?
A: Authentication verifies who someone is. Authorization determines what they can do. Authentication answers "Is this user Jake?" while authorization answers "Can Jake edit this document?" They're related but distinct problems requiring different solutions.
Q: Can authorization systems handle attribute-based access control?
A: Modern relationship-based authorization systems can handle ABAC requirements alongside role-based and relationship-based patterns. Systems like SpiceDB support caveats that enable policy-based checks for attributes like time of day or IP address.
Q: How do centralized authorization systems handle scale?
A: Google's Zanzibar processes 10 million queries per second while storing trillions of relationships. Systems built on these patterns distribute permission data globally and use graph-based data structures optimized for authorization queries. Read more about authorization scaling.
Q: What happens to my existing authorization code during migration?
A: Migration to centralized authorization is typically gradual. You model your permissions in the new system, write both old and new systems in parallel, verify consistency, then cut over service by service. The authorization primer covers migration approaches.
Q: Do I need to be a large company to benefit from proper authorization infrastructure?
A: No. While companies like Google pioneered these approaches at massive scale, the patterns work at any size. Early adoption prevents accumulating authorization technical debt that becomes expensive to fix later. Starting with proper authorization infrastructure is often easier than migrating later.