Best practices for creating clear, performant, and maintainable authorization models.
A well-designed authorization model is easier to understand, debug, and maintain. It also performs better and scales more gracefully as your application grows. This guide covers key principles for modeling authorization in Auth0 FGA.
Core Principle: Model Your Domain, Not a Meta-Model
The most common mistake when starting with Auth0 FGA is creating an overly generic model that can represent “anything.” While this seems flexible, it trades clarity for abstraction and often hurts performance.
Rule of ThumbIf end-users can define it, store it in tuples. If it’s built into your application, define it in the model.For example: built-in roles like “admin” or “billing_manager” should be relations in your model. User-defined custom roles should be stored as tuples with a role type.
Define types and relations that mirror your application’s domain. If your app has organizations, projects, and documents, model exactly that with explicit relationships:Notice how this model has a clear hierarchy (organization → project → document) where each type and relationship directly reflects the application’s domain. Permission inheritance follows a well-defined path that’s easy to understand and audit.This approach has several advantages:
Enhanced clarity and maintainability: Authorization logic is easier to understand, debug, and maintain. Developers and security auditors can readily grasp the meaning of each type and relationship just by reading the model.
Better performance: Models with specific types and flatter hierarchies perform better. Auth0 FGA processes queries more efficiently with well-defined types compared to navigating complex recursive relationships within generic types.
Easier evolution: Auth0 FGA’s modeling language is designed to be adaptable. You can define numerous distinct types and relationships without significant overhead. Model changes rarely require data migrations, allowing you to evolve your model as your application grows.
Team autonomy with modules: Resource types owned by each application team can be maintained in independent modules. You can control which application can write to specific resource types through API credentials, providing better security boundaries.
For roles that come with your application (admin, member, viewer, etc.), define them directly as relations:Adding new built-in roles is straightforward: add a relation to the model. This happens infrequently and doesn’t require data migration.
Some applications let end-users create their own roles. In this case, combine static roles with a dynamic role type:This hybrid approach gives you the clarity of static roles for common cases while supporting custom roles when needed.For more details, see Modeling Roles and Custom Roles.
If customers need hierarchical organizations, prefer explicit types for each level when the structure is well-defined:This makes the hierarchy explicit: organizations contain departments, and department members inherit from the organization.If the hierarchy depth is truly dynamic (customers can nest arbitrarily), then add recursion only where needed:Now you have two distinct hierarchies:
System hierarchy: Non-recursive, for your internal super-admin access
Organization hierarchy: Recursive only if customers truly need arbitrary nesting
Prefer explicit types when possible; use recursion only when the depth is genuinely unbounded.
Applications have different kinds of resources: documents, folders, projects, tickets, accounts, etc. Some have parent-child relationships (folders contain documents, projects contain tickets).