OpenFGA Best Practices
Version 1.0.0 OpenFGA Community January 2026
Note: This document is mainly for agents and LLMs to follow when authoring, generating, or refactoring OpenFGA authorization models. Humans may also find it useful, but guidance here is optimized for automation and consistency by AI-assisted workflows.
Abstract
Comprehensive guide for authoring OpenFGA authorization models, designed for AI agents and LLMs. Covers core concepts, relationship patterns, testing methodologies, custom roles, and model optimization. Each section includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific guidance to ensure correct authorization modeling.
Table of Contents
- Core Concepts — CRITICAL
- Relationship Definitions — CRITICAL
- Model Design — HIGH
- Testing & Validation — HIGH
- 4.1 Structure Tests in .fga.yaml
- 4.2 Check Assertions
- 4.3 List Objects Tests
- 4.4 List Users Tests
- 4.5 Testing Conditions
- 4.6 OpenFGA CLI Usage
- Custom Roles — MEDIUM
- Optimization — MEDIUM
- 6.1 Simplify Models
- 6.2 Minimize Tuple Count
- 6.3 Type Restrictions
- SDK Integration — HIGH
- 7.1 JavaScript/TypeScript SDK
- 7.2 Go SDK
- 7.3 Python SDK
- 7.4 Java SDK
- 7.5 .NET SDK
- Workflow — CRITICAL
1. Core Concepts
Impact: CRITICAL
Understanding core concepts is fundamental to creating correct and maintainable authorization models.
1.1 Define Types for Entity Classes
Impact: CRITICAL (foundation of your model)
Types define classes of objects in your system. Every entity that participates in authorization should have a type.
Incorrect (missing types):
model
schema 1.1
type user
type document
relations
define owner: [user]
define viewer: [user]
This model is missing types for organizational structure that documents might belong to.
Correct (comprehensive types):
model
schema 1.1
type user
type organization
relations
define member: [user]
define admin: [user]
type folder
relations
define owner: [user]
define viewer: [user]
type document
relations
define parent_folder: [folder]
define organization: [organization]
define owner: [user]
define viewer: [user]
Identify all relevant entities: users, resources, organizational units, groups, and any containers.
1.2 Schema Version
Impact: HIGH (enables full feature set)
Always use schema version 1.1 to access all OpenFGA features.
Incorrect (missing schema version):
model
type user
type document
relations
define owner: [user]
Correct (explicit schema version):
model
schema 1.1
type user
type document
relations
define owner: [user]
Schema 1.1 enables conditions, intersection, exclusion, and other advanced features.
1.3 Relations Belong on Object Types
Impact: CRITICAL (correct model structure)
Relations are defined on the types that represent resources being accessed, not on user types.
Incorrect (relations on user type):
model
schema 1.1
type user
relations
define owns_document: [document] # Wrong! Relations go on the resource
Correct (relations on resource type):
model
schema 1.1
type user
type document
relations
define owner: [user] # Correct! Defined on the resource
Ask "Can user U perform action A on object O?" — the relation belongs on type O.
1.4 Relationship Tuples as Facts
Impact: CRITICAL (model vs data)
Relationship tuples represent facts about who has what relationship to what object. They are the data that brings your model to life.
Model defines possibilities:
type document
relations
define owner: [user]
define editor: [user]
Tuples establish facts:
tuples:
- user: user:anne
relation: owner
object: document:roadmap
- user: user:bob
relation: editor
object: document:roadmap
Without tuples, authorization checks will fail because the model only defines what is possible, not what currently exists.
Key distinction:
- Model = static schema defining possible relationships
- Tuples = dynamic data representing actual relationships
1.5 Model vs Data Separation
Impact: HIGH (architectural clarity)
The authorization model (schema) is static and defines structure. Relationship tuples (data) are dynamic and change frequently.
Model characteristics:
- Immutable; each modification creates a new version
- Changes rarely; only when product features change
- Defines the possible relationships
Tuple characteristics:
- Mutable; written and deleted as application state changes
- Changes frequently; as users gain/lose access
- Represents the actual relationships
Example:
# Model (changes rarely)
type document
relations
define owner: [user]
define viewer: [user] or owner
# Tuples (change frequently)
- user: user:anne
relation: owner
object: document:roadmap
# This tuple can be added/removed as permissions change
This separation enables efficient permission evaluation and decouples core logic changes from specific user permission modifications.
2. Relationship Definitions
Impact: CRITICAL
The building blocks for expressing authorization logic in OpenFGA.
2.1 Direct Relationships
Impact: CRITICAL (explicit access grants)
Direct relationships require explicit relationship tuples. Use type restrictions to control what can be directly assigned.
Type restriction patterns:
| Pattern | Meaning | Example |
|---|---|---|
[user] | Only individual users | define owner: [user] |
[user, team#member] | Users or team members | define editor: [user, team#member] |
[organization] | Only organizations | define parent: [organization] |
Example:
type document
relations
define owner: [user]
Tuple to grant access:
- user: user:anne
relation: owner
object: document:roadmap
Without a tuple, user:anne has no owner relationship to document:roadmap.
Common mistake: Forgetting that direct relationships require explicit tuples. The model only defines what is possible.
2.2 Concentric Relationships
Impact: HIGH (permission inheritance)
Use or to create nested permissions where one relation implies another.
Incorrect (redundant tuples required):
type document
relations
define editor: [user]
define viewer: [user]
This requires separate tuples for both editor and viewer access.
Correct (editors inherit viewer access):
type document
relations
define editor: [user]
define viewer: [user] or editor
Now editors automatically have viewer access without additional tuples.
Typical hierarchy:
type document
relations
define owner: [user]
define editor: [user] or owner
define viewer: [user] or editor
Owners can edit and view. Editors can view. Each level inherits from the one above.
Benefits:
- Fewer tuples needed
- Consistent permission semantics
- Easier to reason about access levels
2.3 Indirect Relationships with X from Y
Impact: CRITICAL (scalable hierarchical access)
The X from Y pattern grants access through an intermediary object, enabling hierarchical permissions.
Incorrect (requires tuples on every document):
type folder
relations
define viewer: [user]
type document
relations
define viewer: [user]
Each document needs its own viewer tuples even if they're in the same folder.
Correct (inherit from parent folder):
type folder
relations
define viewer: [user]
type document
relations
define parent_folder: [folder]
define viewer: [user] or viewer from parent_folder
Tuples:
# Grant folder access once
- user: user:anne
relation: viewer
object: folder:engineering
# Link documents to folder
- user: folder:engineering
relation: parent_folder
object: document:spec
- user: folder:engineering
relation: parent_folder
object: document:design
Anne can view all documents in the engineering folder with just one permission tuple.
Common patterns:
viewer from parent_folder- Folder inheritanceadmin from organization- Org-level admin accessmember from team- Team membership propagation
Benefits:
- Dramatically reduces tuple count
- Simplifies permission management
- Enables revoking access by deleting a single tuple
2.4 Usersets for Group-Based Access
Impact: HIGH (efficient group management)
Usersets (type#relation) represent collections of users, enabling group-based access control.
Syntax: type#relation means "all users who have this relation to objects of this type"
Example (team-based access):
type team
relations
define member: [user]
type document
relations
define editor: [user, team#member]
Tuples:
# Add users to team
- user: user:anne
relation: member
object: team:engineering
- user: user:bob
relation: member
object: team:engineering
# Grant team access to document
- user: team:engineering#member
relation: editor
object: document:roadmap
Both Anne and Bob can edit the roadmap through their team membership.
Important: team#member means "members of a specific team". It does NOT mean "must be a team member to be an editor". Only use it when assigning access to a group.
Common mistake:
# This does NOT mean "only team members can be editors"
define editor: [team#member]
# It means "you can assign all members of a specific team as editors"
2.5 Conditional Relationships
Impact: MEDIUM (dynamic authorization)
Conditions use CEL (Common Expression Language) to add runtime context to authorization decisions.
Example (time-based access):
model
schema 1.1
type user
type organization
relations
define admin: [user with non_expired_grant]
condition non_expired_grant(current_time: timestamp, grant_time: timestamp, grant_duration: duration) {
current_time < grant_time + grant_duration
}
Important: Conditions must be defined at the end of the model, after all type definitions.
Conditional tuple:
- user: user:peter
relation: admin
object: organization:acme
condition:
name: non_expired_grant
context:
grant_time: "2024-02-01T00:00:00Z"
grant_duration: 1h
Check with context:
check:
- user: user:peter
object: organization:acme
context:
current_time: "2024-02-01T00:10:00Z"
assertions:
admin: true # Within the 1-hour window
Common use cases:
- Time-based access (expiring grants)
- IP-based restrictions
- Feature flags
- Attribute-based conditions
2.6 Wildcards for Public Access
Impact: LOW (use carefully)
Wildcards (type:*) grant access a all instances of a user type to access a specfic object.
Example (public documents):
type document
relations
define viewer: [user, user:*]
Tuple for public access:
- user: user:*
relation: viewer
object: document:public-readme
All users can view the public-readme document.
Correct usage scenarios:
- Public documentation
- Shared resources everyone should access
- Anonymous/guest access patterns
Incorrect usage (avoid):
# Don't use wildcards as a shortcut for "any user can be assigned"
type document
relations
define editor: [user:*] # Too permissive for editing
2.7 Wildcards for boolean attributes
Impact: MEDIUM (use carefully)
Wildcards (type:*) grant access a all instances of a user type to access a specfic object. They can be used to simulate boolean attributes.
Example (feature entitlements):
type organization
relations
define member: [user]
define feature_sso: [user:*]
define can_access_sso : feature_sso and member
Note that if the permission needs to check both for the 'boolean attribute' (feature_sso) and verify the user is a member of the organization.
**Tuples **
- user: user:anne
relation: member
object: organization:acme
- user: user:*
relation: feature_sso
object: organization:acme
All members from the acme organization can access the 'feature_sso' feature.
Correct usage scenarios:
- Feature Flags
- Entitlements
- Boolean states ('enabled', 'active', 'published')
3. Model Design
Impact: HIGH
Design patterns that lead to maintainable and correct authorization models.
3.1 Define Permissions with can_ Relations
Impact: HIGH (clear permission semantics)
Define specific permissions using can_<action> relations that cannot be directly assigned.
Incorrect (checking relations directly):
type document
relations
define owner: [user]
define editor: [user] or owner
define viewer: [user] or editor
Application checks editor relation but semantics are unclear.
Correct (explicit permissions):
type document
relations
define owner: [user]
define editor: [user] or owner
define viewer: [user] or editor
define can_view: viewer
define can_edit: editor
define can_delete: owner
define can_share: owner
Application code:
// Clear intent - checking specific permissions
await fga.check({ user, relation: 'can_view', object: doc })
await fga.check({ user, relation: 'can_edit', object: doc })
await fga.check({ user, relation: 'can_delete', object: doc })
Benefits:
- Clear separation between roles and permissions
- Permissions can combine multiple roles
- Easier to evolve without breaking applications
- Self-documenting model
Advanced: Permission from multiple sources:
type document
relations
define owner: [user]
define editor: [user]
define org: [organization]
# Permission can come from direct role OR org admin
define can_delete: owner or admin from org
3.2 Hierarchical Structures
Impact: HIGH (scalable permission inheritance)
Model parent-child relationships to enable permission inheritance through hierarchies.
Example (folder hierarchy):
model
schema 1.1
type user
type folder
relations
define parent_folder: [folder]
define owner: [user] or owner from parent_folder
define editor: [user] or owner or editor from parent_folder
define viewer: [user] or editor or viewer from parent_folder
type document
relations
define parent_folder: [folder]
define owner: [user] or owner from parent_folder
define editor: [user] or owner or editor from parent_folder
define viewer: [user] or editor or viewer from parent_folder
Tuples for nested structure:
# Nested folder structure
- user: folder:root
relation: parent_folder
object: folder:engineering
- user: folder:engineering
relation: parent_folder
object: folder:backend
# Document in nested folder
- user: folder:backend
relation: parent_folder
object: document:api-spec
# Grant access at root
- user: user:cto
relation: viewer
object: folder:root
The CTO can view all documents in all nested folders with a single tuple.
Key patterns:
- Parent relations should allow the same type:
define parent_folder: [folder] - Permissions inherit via
X from parent_folder - Each level adds its own direct grants with
[user]
Benefits:
- Single permission grant propagates to entire subtree
- Revoke access by removing one tuple
- Natural mapping to file system structures
3.3 Organization-Level Access
Impact: HIGH (multi-tenant authorization)
Model organization membership and propagate access to owned resources.
Model:
model
schema 1.1
type user
type organization
relations
define member: [user]
define admin: [user]
type project
relations
define organization: [organization]
define owner: [user] or admin from organization
define editor: [user] or owner
define viewer: [user] or editor or member from organization
Tuples:
# Organization membership
- user: user:anne
relation: admin
object: organization:acme
- user: user:bob
relation: member
object: organization:acme
# Project belongs to organization
- user: organization:acme
relation: organization
object: project:website
Access results:
- Anne (admin): can own, edit, and view the project
- Bob (member): can view the project
- All through organization membership
Extended pattern with teams:
type team
relations
define organization: [organization]
define member: [user]
type project
relations
define organization: [organization]
define team: [team]
define viewer: [user] or member from team or member from organization
Multi-tenant isolation:
type organization
relations
define member: [user]
type resource
relations
define organization: [organization]
# Only org members can have any access
define viewer: member from organization
This ensures resources are only visible within their organization.
3.4 Naming Conventions
Impact: MEDIUM (maintainability)
Use consistent naming conventions for clarity and maintainability.
Types - use singular nouns in lowercase:
# Correct
type user
type document
type folder
type organization
type team
type project
# Incorrect
type Users # No capitals
type documents # Not plural
type FOLDER # Not all caps
Relations - use descriptive names:
type document
relations
# Roles (who someone is)
define owner: [user]
define editor: [user]
define viewer: [user]
define admin: [user]
define member: [user]
# Structural (relationships between objects)
define parent_folder: [folder]
define organization: [organization]
define parent: [document]
# Permissions (what someone can do)
define can_view: viewer
define can_edit: editor
define can_delete: owner
define can_share: owner
Object identifiers - use meaningful, readable IDs:
# Good
- object: document:roadmap-2024
- object: organization:acme-corp
- object: folder:engineering-docs
- object: user:anne-smith
# Avoid
- object: document:12345 # Meaningless ID
- object: organization:org1 # Non-descriptive
- object: folder:f_001 # Cryptic
Consistency guidelines:
- Use snake_case for multi-word relations:
parent_folder,can_view - Use kebab-case for object IDs:
roadmap-2024,acme-corp - Prefix permissions with
can_:can_view,can_edit,can_delete - Use nouns for roles:
owner,editor,viewer,admin
3.5 Modularize your modules with 'modules'
Impact: MEDIUM (multiple-team collaboration)
CRITICAL: Only do this when you are asked to modularize the model. By default, create models in a single file.
Split the authorization model in modules
Create a module for each sub-domain an application.
Define a 'core.fga' module with the types that should be used by all sub-domains (e.g. organization, role, group), and an individual '.fga' file for each sub-domain.
Create an 'fga.mod' file that includes all '.fga' files:
schema: '1.2'
contents:
- core.fga
- wiki.fga
Module Definition
Modules are stored in .fga. files and start with the module keyword
core.fga
module core
type user
type organization
relations
define member: [user]
define admin: [user]
type group
relations
define member: [user]
Extending types from other modules
Each sub-domain frequently need to define top-level permission that are defined, for example, at the organization type. They can do by using the extend type syntax:
wiki.fga
module wiki
extend type organization
relations
define can_create_space: admin
type space
relations
define organization: [organization]
define can_view_pages: member from organization
type page
relations
define space: [space]
define owner: [user]
- A single type can only be extended once per file
- The relations added must not already exist, or be part of another type extension
Testing Models with Modules
When creating .fga.yaml files to test models that include modular models, you need to point to the fga.mod file:
name: ModularDemo
model_file: ./fga.mod
tuples:
- user: user:anne
relation: admin
object: organization:openfga
object: project:openfga
tests:
- name: Members can view projects
check:
- user: user:anne
object: organization:openfga
assertions:
admin: true
member: true
can_create_space: true
4. Testing & Validation
Impact: HIGH
Thorough testing ensures your authorization model behaves as expected.
4.1 Structure Tests in .fga.yaml
Impact: HIGH (test-driven authorization)
The .fga.yaml file defines both your model and tests in a single file.
Basic structure:
name: My Authorization Model Tests
model: |
model
schema 1.1
type user
type document
relations
define owner: [user]
define editor: [user] or owner
define viewer: [user] or editor
tuples:
- user: user:anne
relation: owner
object: document:roadmap
- user: user:bob
relation: editor
object: document:roadmap
tests:
- name: Document access tests
check:
# Check assertions here
list_objects:
# List objects assertions here
list_users:
# List users assertions here
Alternative (external files):
name: Model Tests
model_file: ./model.fga
tuple_file: ./tuples.yaml
Alternative when using Modular Models:
name: Model Tests
model_file: ./fga.mod
tuple_file: ./tuples.yaml
Multiple tuple files:
tuple_files:
- ./users.yaml
- ./permissions.yaml
- ./org-structure.yaml
Benefits:
- Self-contained test definitions
- Version-controlled authorization logic
- Enables test-driven development for authorization
4.2 Check Assertions
Impact: HIGH (verify permission grants)
Check assertions verify whether a user has a specific relation to an object.
Example:
tests:
- name: Owner permissions
check:
- user: user:anne
object: document:roadmap
assertions:
owner: true
editor: true # Inherited through concentric relationship
viewer: true # Inherited through concentric relationship
can_delete: true
- user: user:bob
object: document:roadmap
assertions:
owner: false
editor: true
viewer: true
can_delete: false
Always test both positive and negative cases:
check:
# Positive: user HAS access
- user: user:anne
object: document:secret
assertions:
viewer: true
# Negative: user does NOT have access
- user: user:mallory
object: document:secret
assertions:
viewer: false
editor: false
owner: false
Test boundary conditions:
check:
# User with no tuples at all
- user: user:unknown
object: document:roadmap
assertions:
viewer: false
# Object with no tuples at all
- user: user:anne
object: document:nonexistent
assertions:
viewer: false
4.3 List Objects Tests
Impact: MEDIUM (verify object enumeration)
List objects tests verify which objects a user has access to.
Example:
tests:
- name: List accessible documents
list_objects:
- user: user:anne
type: document
assertions:
owner:
- document:roadmap
viewer:
- document:roadmap
- document:public-doc
- user: user:bob
type: document
assertions:
owner: [] # Empty list - no owned documents
editor:
- document:roadmap
Test empty results:
list_objects:
- user: user:unknown
type: document
assertions:
owner: []
viewer: []
Test multiple object types:
list_objects:
- user: user:anne
type: document
assertions:
viewer:
- document:roadmap
- document:spec
- user: user:anne
type: folder
assertions:
viewer:
- folder:engineering
Use cases:
- Building UI that shows accessible resources
- Auditing user access across the system
- Verifying hierarchical inheritance works correctly
4.4 List Users Tests
Impact: MEDIUM (verify user enumeration)
List users tests verify which users have access to an object.
Example:
tests:
- name: List document users
list_users:
- object: document:roadmap
user_filter:
- type: user
assertions:
owner:
users:
- user:anne
editor:
users:
- user:anne
- user:bob
viewer:
users:
- user:anne
- user:bob
Test empty results:
list_users:
- object: document:private
user_filter:
- type: user
assertions:
viewer:
users: []
User filter with relation (for usersets):
list_users:
- object: document:roadmap
user_filter:
- type: team
relation: member
assertions:
editor:
users:
- team:engineering#member
User filter formats:
type: user- List individual userstype: teamwithrelation: member- List team usersetstype: userwithuser:*- Include public access
Use cases:
- Auditing who has access to sensitive resources
- Building share dialogs showing current collaborators
- Compliance reporting
4.5 Testing Conditions
Impact: MEDIUM (verify dynamic authorization)
Test conditional relationships by providing context in your assertions.
Example model:
model
schema 1.1
type user
type resource
relations
define viewer: [user with in_allowed_ip_range]
condition in_allowed_ip_range(user_ip: string, allowed_range: string) {
user_ip.startsWith(allowed_range)
}
Conditional tuple:
tuples:
- user: user:anne
relation: viewer
object: resource:internal
condition:
name: in_allowed_ip_range
context:
allowed_range: "192.168."
Tests with context:
tests:
- name: Conditional access tests
check:
# Access granted - IP matches
- user: user:anne
object: resource:internal
context:
user_ip: "192.168.1.100"
assertions:
viewer: true
# Access denied - IP doesn't match
- user: user:anne
object: resource:internal
context:
user_ip: "10.0.0.50"
assertions:
viewer: false
Time-based condition testing:
tests:
- name: Time-based access
check:
# Within valid window
- user: user:peter
object: organization:acme
context:
current_time: "2024-02-01T00:10:00Z"
assertions:
admin: true
# After window expired
- user: user:peter
object: organization:acme
context:
current_time: "2024-02-02T00:00:00Z"
assertions:
admin: false
Always test both passing and failing condition evaluations.
4.6 OpenFGA CLI Usage
Impact: HIGH (validation workflow)
Use the OpenFGA CLI to validate and test your models.
MANDATORY: Always run fga model test after creating or modifying any .fga or .fga.yaml file. Do not consider any OpenFGA task complete until tests pass.
Use the OpenFGA CLI to validate and test your models.
Installation:
# macOS
brew install openfga/tap/fga
# Debian
sudo apt install ./fga_<version>_linux_<arch>.deb
# Docker
docker pull openfga/cli
docker run -it openfga/cli
Validate model syntax:
fga model validate --file model.fga
Run tests:
fga model test --tests model.fga.yaml
Transform between formats:
# DSL to JSON
fga model transform --input model.fga --output model.json
# JSON to DSL
fga model transform --input model.json --output model.fga
Example test run:
$ fga model test --tests authorization.fga.yaml
# Test Summary #
Tests 1/1 passing
Checks 5/5 passing
CI/CD integration:
# Fail the build if tests don't pass
fga model test --tests authorization.fga.yaml || exit 1
You can also use the available GitHub actions.
Verbose output for debugging:
fga model test --tests authorization.fga.yaml --verbose
5. Custom Roles
Impact: MEDIUM
Implement user-defined roles when applications need flexible permission structures.
5.1 Simple Static Roles
Impact: MEDIUM (organization-wide roles)
Always start with static roles defined in each type, unless you are asked to support custom-roles or user-defined roles
Model:
model
schema 1.1
type user
type organization
relations
define admin: [user] # Static role
define member: [user] # Static role
type project
relations
define organization: [organization]
define owner: [user]
define editor: [user]
Setting up a static role:
# 1. Define role permissions
- user: user:anne
relation: admin
object: organization:acme
- user: user:bob
relation: admin
object: project:website
Use when:
- Roles apply at the organization level
- Same role permissions everywhere
- Simple permission structure
5.2 Role Assignments for Resource-Specific Roles
Impact: MEDIUM (per-resource role members)
For roles that can have different members on different levels of a resource hierarchy. DO NOT use this for top-level types like organizations.
Model:
model
schema 1.1
type user
type role
relations
define can_view_project: [user:*]
define can_edit_project: [user:*]
type role_assignment
relations
define assignee: [user]
define role: [role]
define can_view_project: assignee and can_view_project from role
define can_edit_project: assignee and can_edit_project from role
type organization
relations
define admin: [user]
type project
relations
define organization: [organization]
define role_assignment: [role_assignment]
define can_edit_project: can_edit_project from role_assignment or admin from organization
define can_view_project: can_view_project from role_assignment or admin from organization
Step 1: Define the role's permissions:
- user: user:*
relation: can_view_project
object: role:project-admin
- user: user:*
relation: can_edit_project
object: role:project-admin
Step 2: Create role assignment with user and role:
- user: user:anne
relation: assignee
object: role_assignment:project-admin-website
- user: role:project-admin
relation: role
object: role_assignment:project-admin-website
Step 3: Link role assignment to project:
- user: role_assignment:project-admin-website
relation: role_assignment
object: project:website
Step 4: Link project to organization:
- user: organization:acme
relation: organization
object: project:website
Use when:
- Different users need the same role on different resources
- Per-project or per-team role membership varies
5.3 Combining Static and Custom Roles
Impact: HIGH (practical role systems)
Combine pre-defined static roles with user-defined custom roles for practical authorization systems.
Model:
model
schema 1.1
type user
type role
relations
define assignee: [user]
type organization
relations
# Static roles - known at design time
define owner: [user]
define admin: [user] or owner
define member: [user] or admin
# Permissions: combine static roles and custom roles
define can_manage_billing: [role#assignee] or owner
define can_manage_members: [role#assignee] or admin
define can_view_analytics: [role#assignee] or member
define can_create_projects: [role#assignee] or member
Static roles provide baseline permissions:
# Org owner has all permissions through static role
- user: user:founder
relation: owner
object: organization:acme
# Admin has member permissions through concentric relationship
- user: user:cto
relation: admin
object: organization:acme
Custom roles extend for specific needs:
# Create a "billing-admin" custom role
- user: role:acme-billing-admin#assignee
relation: can_manage_billing
object: organization:acme
# Assign user to the custom role
- user: user:accountant
relation: assignee
object: role:acme-billing-admin
Benefits:
- Static roles handle common patterns (owner, admin, member)
- Custom roles allow organizational flexibility
- Clear separation of concerns
- Easier to understand and audit
Recommendation: Always define static roles for known, common access patterns. Use custom roles for organization-specific extensions.
5.4 When to Use Each Role Pattern
Impact: MEDIUM (choosing the right pattern)
| Pattern | Use Case | Pros | Cons |
|---|---|---|---|
| Simple Static Roles | Organization-wide roles with consistent permissions | Simple, efficient | Less flexible for per-resource customization |
| Simple Custom Roles | Custom organization-wide roles with consistent permissions | Simple, efficient | Less flexible for per-resource customization |
| Role Assignments | Custom resource-specific roles with different members per resource | Highly flexible | More complex, more tuples |
Choose Simple Roles when:
- Roles apply at the organization level
- Same users have the role everywhere
- Permission structure is straightforward
- You want minimal tuple management
Example: Organization billing admin, HR manager
Choose Custom Roles when:
- You need to let end-users define their own roles at the organization level
- Same users have the role everywhere
- Permission structure is straightforward
- You want minimal tuple management
Example: End-users can create a billing admin or HR admin role
Choose Role Assignments when:
- You need to let end-users define their own roles at the organization level
- Different users need the same role on different resources
- Per-project or per-team role membership varies
- Fine-grained resource-level control is required
- Role membership changes frequently per resource
Example: Project lead (different for each project)
Migration strategies when evolving roles:
- Additive approach: Introduce custom roles alongside existing static roles
- Gradual migration: Move permissions one at a time to custom roles
- Backwards compatibility: Maintain existing static role behavior during transition
Common mistake: Using role assignments for organization-level roles. This adds unnecessary complexity. Use simple user-defined roles instead.
6. Optimization
Impact: MEDIUM
Optimize your models for clarity and efficiency.
6.1 Simplify Models
Impact: MEDIUM (maintainability)
Remove unused types and relations from your model.
Incorrect (unused relations):
type document
relations
define owner: [user]
define editor: [user]
define viewer: [user]
define commenter: [user] # Never used in application
define legacy_admin: [user] # Deprecated, no tuples exist
define can_view : owner or editor or viewer
Correct (minimal model):
type document
relations
define owner: [user]
define editor: [user]
define viewer: [user]
define can_view : owner or editor or viewer
Audit checklist:
- Unused types: Remove types that have no tuples and aren't referenced
- Unused relations: Remove relations that are never checked or written
- Unreferenced conditions: Remove conditions not used in any relation
- Dead paths: Remove
X from Ypaths where Y relation is never used
After generating models and tests:
# Check which relations are actually tested
grep -r "relation:" tests/*.yaml | sort | uniq
# Compare against model relations
fga model validate --file model.fga
Benefits:
- Easier to understand and maintain
- Faster validation
- Clearer documentation
- Reduced confusion for developers
6.2 Minimize Tuple Count
Impact: MEDIUM (storage and performance)
Use indirect relationships to reduce the number of tuples needed.
Incorrect (tuple explosion):
# Granting viewer access to 100 documents individually
- user: user:anne
relation: viewer
object: document:doc-1
- user: user:anne
relation: viewer
object: document:doc-2
# ... 98 more tuples (100 total!)
Correct (hierarchical access):
# Grant folder access once
- user: user:anne
relation: viewer
object: folder:engineering
# Documents inherit from folder (structural tuples)
- user: folder:engineering
relation: parent_folder
object: document:doc-1
- user: folder:engineering
relation: parent_folder
object: document:doc-2
# ... link documents to folder
One permission tuple + structural tuples scales better than individual grants.
Team-based access:
# Instead of 50 individual user grants:
- user: user:alice
relation: viewer
object: document:spec
- user: user:bob
relation: viewer
object: document:spec
# ... 48 more
# Use team membership:
- user: team:engineering#member
relation: viewer
object: document:spec
# Add users to team separately
- user: user:alice
relation: member
object: team:engineering
Benefits:
- Fewer tuples to store and query
- Easier permission management
- Single point of revocation
- Better performance at scale
6.3 Type Restrictions
Impact: LOW (model correctness)
Apply appropriate type restrictions to prevent invalid tuples.
Incorrect (overly permissive):
type document
relations
define parent: [folder, document, user, organization] # Too broad
Correct (precise restrictions):
type document
relations
define parent_folder: [folder] # Only folders can be parents
define organization: [organization] # Only organizations
define owner: [user] # Only users can own
If business rules implies that a resource can belong to different kind of parents, then it is OK to represent it in the model:
type organization
define member: [user]
type business_unit
define member: [user]
type document
relations
define parent_entity: [organization, business_unit]
define parent_folder: [folder] # Only folders can be parents
define owner: [user] # Only users can own
define can_view: owner or member from parent_entity
Common type restriction patterns:
# Only users
define owner: [user]
# Users or usersets (team members)
define editor: [user, team#member]
# Only organizational objects
define parent: [organization]
# Users with conditions
define admin: [user with time_based]
# Public access (use carefully)
define viewer: [user, user:*]
Type restrictions:
- Prevent invalid tuples from being written
- Make the model self-documenting
- Enable better tooling support
- Catch errors at write time, not check time
Anti-pattern - kitchen sink:
# Don't do this - too permissive
define viewer: [user, user:*, team, team#member, organization, organization#member, role#assignee]
Instead, be specific about what types make sense for each relation.
7. SDK Integration
Impact: HIGH
SDK implementations for integrating OpenFGA into your applications.
7.1 JavaScript/TypeScript SDK
Impact: HIGH (client implementation for JS/TS)
The @openfga/sdk package provides the official OpenFGA client for JavaScript and TypeScript applications.
Installation
npm install @openfga/sdk
Client Initialization
Basic setup:
const { OpenFgaClient } = require('@openfga/sdk');
const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID,
});
With API Token:
const { OpenFgaClient, CredentialsMethod } = require('@openfga/sdk');
const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
storeId: process.env.FGA_STORE_ID,
credentials: {
method: CredentialsMethod.ApiToken,
config: {
token: process.env.FGA_API_TOKEN,
}
}
});
With Client Credentials (OAuth2):
const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
storeId: process.env.FGA_STORE_ID,
credentials: {
method: CredentialsMethod.ClientCredentials,
config: {
apiTokenIssuer: process.env.FGA_API_TOKEN_ISSUER,
apiAudience: process.env.FGA_API_AUDIENCE,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
}
}
});
Load Authorization Model from File
From JSON file:
const fs = require('fs');
// Read and parse JSON model
const modelJson = JSON.parse(fs.readFileSync('model.json', 'utf8'));
const { authorization_model_id } = await fgaClient.writeAuthorizationModel(modelJson);
From DSL (.fga) file:
Use the @openfga/syntax-transformer package to convert DSL to JSON:
npm install @openfga/syntax-transformer
const fs = require('fs');
const { transformer } = require('@openfga/syntax-transformer');
// Read DSL file and transform to JSON
const dslContent = fs.readFileSync('model.fga', 'utf8');
const modelJson = transformer.transformDSLToJSON(dslContent);
const { authorization_model_id } = await fgaClient.writeAuthorizationModel(
JSON.parse(modelJson)
);
Alternative: Use CLI for conversion
# Convert DSL to JSON using the FGA CLI
fga model transform --input model.fga --output model.json
Then load the JSON file as shown above.
Check Permission
const result = await fgaClient.check({
user: "user:anne",
relation: "viewer",
object: "document:roadmap",
});
// result.allowed === true or false
Batch Check
const { result } = await fgaClient.batchCheck({
checks: [
{ user: "user:anne", relation: "viewer", object: "document:roadmap" },
{ user: "user:bob", relation: "editor", object: "document:budget" }
]
});
Write Tuples
await fgaClient.write({
writes: [
{ user: "user:anne", relation: "viewer", object: "document:roadmap" }
],
deletes: [
{ user: "user:bob", relation: "editor", object: "document:budget" }
],
});
// Convenience methods
await fgaClient.writeTuples([
{ user: "user:anne", relation: "viewer", object: "document:roadmap" }
]);
await fgaClient.deleteTuples([
{ user: "user:bob", relation: "editor", object: "document:budget" }
]);
List Objects
const response = await fgaClient.listObjects({
user: "user:anne",
relation: "viewer",
type: "document",
});
// response.objects = ["document:roadmap", "document:budget"]
List Relations
const response = await fgaClient.listRelations({
user: "user:anne",
object: "document:roadmap",
relations: ["can_view", "can_edit", "can_delete"],
});
// response.relations = ["can_view", "can_edit"]
List Users
const response = await fgaClient.listUsers({
object: { type: "document", id: "roadmap" },
relation: "can_read",
user_filters: [{ type: "user" }],
});
// response.users = [{ object: { type: "user", id: "anne" } }]
Read Tuples
const { tuples } = await fgaClient.read({
user: "user:anne",
relation: "viewer",
object: "document:roadmap",
});
Non-Transaction Write Mode
For large batch writes:
const response = await fgaClient.write({
writes: largeTupleArray,
}, {
transaction: {
disable: true,
maxPerChunk: 100,
maxParallelRequests: 10,
}
});
Handle Write Conflicts
const { ClientWriteRequestOnDuplicateWrites } = require('@openfga/sdk');
await fgaClient.write({
writes: [{ user: "user:anne", relation: "writer", object: "document:budget" }],
}, {
conflict: {
onDuplicateWrites: ClientWriteRequestOnDuplicateWrites.Ignore,
}
});
Retry Configuration
const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
retryParams: {
maxRetry: 3,
minWaitInMs: 250
}
});
Best Practices
- Initialize once: Create
OpenFgaClientonce and reuse throughout your application - Input format: Parameters use camelCase
- Response format: API responses use snake_case
- Retry behavior: SDK auto-retries on 429 and 5xx errors (up to 3 times)
- Batch operations: Use
correlationIdto match responses to requests
7.2 Go SDK
Impact: HIGH (client implementation for Go)
The github.com/openfga/go-sdk package provides the official OpenFGA client for Go applications.
Installation
go get -u github.com/openfga/go-sdk
go mod tidy
Client Initialization
Basic setup:
import openfga "github.com/openfga/go-sdk"
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"),
StoreId: os.Getenv("FGA_STORE_ID"),
AuthorizationModelId: os.Getenv("FGA_MODEL_ID"),
})
With API Token:
import "github.com/openfga/go-sdk/credentials"
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"),
StoreId: os.Getenv("FGA_STORE_ID"),
Credentials: &credentials.Credentials{
Method: credentials.CredentialsMethodApiToken,
Config: &credentials.Config{
ApiToken: os.Getenv("FGA_API_TOKEN"),
},
},
})
With Client Credentials (OAuth2):
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"),
Credentials: &credentials.Credentials{
Method: credentials.CredentialsMethodClientCredentials,
Config: &credentials.Config{
ClientCredentialsClientId: os.Getenv("FGA_CLIENT_ID"),
ClientCredentialsClientSecret: os.Getenv("FGA_CLIENT_SECRET"),
ClientCredentialsApiAudience: os.Getenv("FGA_API_AUDIENCE"),
ClientCredentialsApiTokenIssuer: os.Getenv("FGA_API_TOKEN_ISSUER"),
},
},
})
Check Permission
data, err := fgaClient.Check(context.Background()).
Body(ClientCheckRequest{
User: "user:anne",
Relation: "viewer",
Object: "document:roadmap",
}).
Execute()
fmt.Printf("allowed: %t", data.GetAllowed())
Batch Check
body := ClientBatchCheckRequest{
Checks: []ClientBatchCheckItem{{
CorrelationId: "check-1",
User: "user:anne",
Relation: "viewer",
Object: "document:roadmap",
}},
}
data, err := fgaClient.BatchCheck(context.Background()).Body(body).Execute()
// Results keyed by correlationId
Write Tuples
body := ClientWriteRequest{
Writes: &[]ClientTupleKey{{
User: "user:anne",
Relation: "viewer",
Object: "document:roadmap",
}},
Deletes: &[]ClientTupleKeyWithoutCondition{{
User: "user:bob",
Relation: "editor",
Object: "document:budget",
}},
}
err := fgaClient.Write(context.Background()).Body(body).Execute()
List Objects
data, err := fgaClient.ListObjects(context.Background()).
Body(ClientListObjectsRequest{
User: "user:anne",
Relation: "can_read",
Type: "document",
}).
Execute()
// data.Objects contains accessible object IDs
Streamed List Objects
response, err := fgaClient.StreamedListObjects(context.Background()).
Body(ClientStreamedListObjectsRequest{
User: "user:anne",
Relation: "can_read",
Type: "document",
}).
Execute()
defer response.Close()
for obj := range response.Objects {
objects = append(objects, obj.Object)
}
List Relations
data, err := fgaClient.ListRelations(context.Background()).
Body(ClientListRelationsRequest{
User: "user:anne",
Object: "document:roadmap",
Relations: []string{"can_view", "can_edit"},
}).
Execute()
// data.Relations contains applicable relations
List Users
data, err := fgaClient.ListUsers(context.Background()).
Body(ClientListUsersRequest{
Object: openfga.FgaObject{Type: "document", Id: "roadmap"},
Relation: "can_read",
UserFilters: []openfga.UserTypeFilter{{Type: "user"}},
}).
Execute()
Read Tuples
data, err := fgaClient.Read(context.Background()).
Body(ClientReadRequest{
User: openfga.PtrString("user:anne"),
Relation: openfga.PtrString("viewer"),
Object: openfga.PtrString("document:roadmap"),
}).
Execute()
Non-Transaction Write Mode
options := ClientWriteOptions{
Transaction: &TransactionOptions{
Disable: true,
MaxParallelRequests: 5,
MaxPerChunk: 100,
},
}
data, err := fgaClient.Write(context.Background()).
Body(body).
Options(options).
Execute()
Load Authorization Model from File
From JSON file:
import (
"encoding/json"
"os"
openfga "github.com/openfga/go-sdk"
)
// Read JSON file
jsonContent, err := os.ReadFile("model.json")
if err != nil {
log.Fatal(err)
}
// Parse into request body
var body openfga.WriteAuthorizationModelRequest
if err := json.Unmarshal(jsonContent, &body); err != nil {
log.Fatal(err)
}
// Write the model
response, err := fgaClient.WriteAuthorizationModel(context.Background()).
Body(body).
Execute()
From DSL (.fga) file:
Install the language transformer:
go get github.com/openfga/language/pkg/go/transformer
import (
"encoding/json"
"os"
"github.com/openfga/language/pkg/go/transformer"
openfga "github.com/openfga/go-sdk"
)
// Read DSL file
dslContent, err := os.ReadFile("model.fga")
if err != nil {
log.Fatal(err)
}
// Transform DSL to JSON
jsonModel, err := transformer.TransformDSLToJSON(string(dslContent))
if err != nil {
log.Fatal(err)
}
// Parse into request body
var body openfga.WriteAuthorizationModelRequest
if err := json.Unmarshal([]byte(jsonModel), &body); err != nil {
log.Fatal(err)
}
// Write the model
response, err := fgaClient.WriteAuthorizationModel(context.Background()).
Body(body).
Execute()
Contextual Tuples
body := ClientCheckRequest{
User: "user:anne",
Relation: "viewer",
Object: "document:roadmap",
ContextualTuples: &[]ClientTupleKey{{
User: "user:anne",
Relation: "editor",
Object: "document:roadmap",
}},
}
Retry Configuration
fgaClient, err := NewSdkClient(&ClientConfiguration{
RetryParams: &openfga.RetryParams{
MaxRetry: 3,
MinWaitInMs: 250,
},
})
Best Practices
- Initialize once: Create the client once and reuse throughout your application
- Use context: Always pass
context.Contextfor cancellation and timeouts - Pointer helpers: Use
openfga.PtrString()for optional string parameters - Retry behavior: SDK auto-retries on 429 and 5xx errors (up to 3 times)
- Streaming: Use
StreamedListObjectsfor large result sets
7.3 Python SDK
Impact: HIGH (client implementation for Python)
The openfga_sdk package provides the official OpenFGA client for Python applications with both async and sync support.
Installation
pip install openfga_sdk
Client Initialization
Async client (recommended):
from openfga_sdk import ClientConfiguration, OpenFgaClient
async def main():
configuration = ClientConfiguration(
api_url="http://localhost:8080",
store_id="YOUR_STORE_ID",
authorization_model_id="YOUR_MODEL_ID"
)
async with OpenFgaClient(configuration) as fga_client:
result = await fga_client.check(body)
return result
Synchronous client:
from openfga_sdk.client import ClientConfiguration
from openfga_sdk.sync import OpenFgaClient
def main():
configuration = ClientConfiguration(
api_url="http://localhost:8080",
store_id="YOUR_STORE_ID"
)
with OpenFgaClient(configuration) as fga_client:
result = fga_client.check(body)
return result
With API Token:
from openfga_sdk.credentials import Credentials, CredentialConfiguration
configuration = ClientConfiguration(
api_url="http://localhost:8080",
credentials=Credentials(
method='api_token',
configuration=CredentialConfiguration(
api_token="YOUR_TOKEN"
)
)
)
With Client Credentials (OAuth2):
configuration = ClientConfiguration(
api_url="http://localhost:8080",
credentials=Credentials(
method='client_credentials',
configuration=CredentialConfiguration(
api_issuer="YOUR_ISSUER",
api_audience="YOUR_AUDIENCE",
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET"
)
)
)
Load Authorization Model from File
From JSON file:
import json
from openfga_sdk import WriteAuthorizationModelRequest
# Read JSON file
with open('model.json', 'r') as f:
model_json = json.load(f)
# Create request from JSON
body = WriteAuthorizationModelRequest(
schema_version=model_json.get('schema_version', '1.1'),
type_definitions=model_json['type_definitions'],
conditions=model_json.get('conditions')
)
response = await fga_client.write_authorization_model(body)
# response.authorization_model_id contains the new model ID
From DSL (.fga) file:
The Python SDK does not include a built-in DSL parser. Convert DSL files to JSON using the OpenFGA CLI, then load the JSON file.
# Convert DSL to JSON using the FGA CLI
fga model transform --input model.fga --output model.json
Then load the JSON file as shown above.
Check Permission
from openfga_sdk.client.models import ClientCheckRequest
body = ClientCheckRequest(
user="user:anne",
relation="viewer",
object="document:roadmap",
)
response = await fga_client.check(body)
# response.allowed = True/False
Batch Check
from openfga_sdk.client.models import (
ClientBatchCheckItem,
ClientBatchCheckRequest
)
checks = [
ClientBatchCheckItem(
user="user:anne",
relation="viewer",
object="document:roadmap"
),
ClientBatchCheckItem(
user="user:bob",
relation="editor",
object="document:budget"
)
]
response = await fga_client.batch_check(
ClientBatchCheckRequest(checks=checks)
)
Write Tuples
from openfga_sdk.client.models import ClientTuple, ClientWriteRequest
body = ClientWriteRequest(
writes=[
ClientTuple(
user="user:anne",
relation="viewer",
object="document:roadmap"
)
],
deletes=[
ClientTuple(
user="user:bob",
relation="editor",
object="document:budget"
)
]
)
response = await fga_client.write(body)
List Objects
from openfga_sdk.client.models import ClientListObjectsRequest
body = ClientListObjectsRequest(
user="user:anne",
relation="viewer",
type="document"
)
response = await fga_client.list_objects(body)
# response.objects = ["document:roadmap", "document:budget"]
Stream List Objects
request = ClientListObjectsRequest(
user="user:anne",
relation="viewer",
type="document"
)
results = []
async for response in fga_client.streamed_list_objects(request):
results.append(response.object)
List Relations
from openfga_sdk.client.models import ClientListRelationsRequest
body = ClientListRelationsRequest(
user="user:anne",
object="document:roadmap",
relations=["can_view", "can_edit"]
)
response = await fga_client.list_relations(body)
# response.relations = ["can_view"]
List Users
from openfga_sdk.client.models import ClientListUsersRequest, UserTypeFilter
from openfga_sdk.models.fga_object import FgaObject
request = ClientListUsersRequest(
object=FgaObject(type="document", id="roadmap"),
relation="can_read",
user_filters=[
UserTypeFilter(type="user"),
UserTypeFilter(type="team", relation="member")
]
)
response = await fga_client.list_users(request)
Read Tuples
from openfga_sdk import ReadRequestTupleKey
body = ReadRequestTupleKey(
user="user:anne",
relation="viewer",
object="document:roadmap"
)
response = await fga_client.read(body)
# response.tuples = [Tuple(...), ...]
Non-Transaction Write Mode
from openfga_sdk.client.models import WriteTransactionOpts
options = {
"transaction": WriteTransactionOpts(
disabled=True,
max_parallel_requests=10,
max_per_chunk=100
)
}
response = await fga_client.write(body, options)
Handle Write Conflicts
from openfga_sdk.client.models.write_conflict_opts import (
ConflictOptions,
ClientWriteRequestOnDuplicateWrites,
ClientWriteRequestOnMissingDeletes
)
options = {
"conflict": ConflictOptions(
on_duplicate_writes=ClientWriteRequestOnDuplicateWrites.IGNORE,
on_missing_deletes=ClientWriteRequestOnMissingDeletes.IGNORE
)
}
response = await fga_client.write(body, options)
Retry Configuration
from openfga_sdk.configuration import RetryParams
config = ClientConfiguration(
api_url="http://localhost:8080",
retry_params=RetryParams(
max_retry=5,
min_wait_in_ms=250
)
)
Error Handling
from openfga_sdk.exceptions import ApiException
try:
await fga_client.check(request)
except ApiException as e:
if e.is_validation_error():
print(f"Validation error: {e.error_message}")
elif e.is_retryable():
print(f"Temporary error (Request: {e.request_id})")
else:
print(f"Error: {e}")
Best Practices
- Use async: Prefer async client for better performance
- Context manager: Use
async withorwithfor proper resource cleanup - Retry behavior: SDK auto-retries on 429 and 5xx errors (up to 3 times)
- Streaming: Use
streamed_list_objectsfor large result sets
7.4 Java SDK
Impact: HIGH (client implementation for Java)
The OpenFGA Java SDK provides the official client for JVM applications. Requires Java 11+.
Installation
Maven:
<dependency>
<groupId>dev.openfga</groupId>
<artifactId>openfga-sdk</artifactId>
<version>0.7.0</version>
</dependency>
Gradle:
implementation 'dev.openfga:openfga-sdk:0.7.0'
Client Initialization
Basic setup:
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL"))
.storeId(System.getenv("FGA_STORE_ID"))
.authorizationModelId(System.getenv("FGA_MODEL_ID"));
var fgaClient = new OpenFgaClient(config);
With API Token:
import dev.openfga.sdk.api.configuration.Credentials;
import dev.openfga.sdk.api.configuration.ApiToken;
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL"))
.storeId(System.getenv("FGA_STORE_ID"))
.credentials(new Credentials(
new ApiToken(System.getenv("FGA_API_TOKEN"))));
var fgaClient = new OpenFgaClient(config);
With Client Credentials (OAuth2):
import dev.openfga.sdk.api.configuration.ClientCredentials;
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL"))
.credentials(new Credentials(
new ClientCredentials()
.apiTokenIssuer(System.getenv("FGA_API_TOKEN_ISSUER"))
.apiAudience(System.getenv("FGA_API_AUDIENCE"))
.clientId(System.getenv("FGA_CLIENT_ID"))
.clientSecret(System.getenv("FGA_CLIENT_SECRET"))));
var fgaClient = new OpenFgaClient(config);
Load Authorization Model from File
From JSON file:
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.openfga.sdk.api.model.WriteAuthorizationModelRequest;
import java.io.File;
ObjectMapper mapper = new ObjectMapper();
// Read and parse JSON file
WriteAuthorizationModelRequest body = mapper.readValue(
new File("model.json"),
WriteAuthorizationModelRequest.class
);
var response = fgaClient.writeAuthorizationModel(body).get();
// response.getAuthorizationModelId() contains the new model ID
From DSL (.fga) file:
Use the openfga-language package to transform DSL to JSON.
Maven:
<dependency>
<groupId>dev.openfga</groupId>
<artifactId>openfga-language</artifactId>
<version>0.2.0</version>
</dependency>
Gradle:
implementation 'dev.openfga:openfga-language:0.2.0'
Transform DSL to JSON:
import dev.openfga.language.DslToJsonTransformer;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.openfga.sdk.api.model.WriteAuthorizationModelRequest;
import java.nio.file.Files;
import java.nio.file.Path;
// Read DSL file
String dslContent = Files.readString(Path.of("model.fga"));
// Transform DSL to JSON
String jsonString = new DslToJsonTransformer().transform(dslContent);
// Parse JSON into request body
ObjectMapper mapper = new ObjectMapper();
WriteAuthorizationModelRequest body = mapper.readValue(
jsonString,
WriteAuthorizationModelRequest.class
);
var response = fgaClient.writeAuthorizationModel(body).get();
// response.getAuthorizationModelId() contains the new model ID
Validate DSL before transforming:
import dev.openfga.language.validation.ModelValidator;
import dev.openfga.language.errors.DslErrorsException;
try {
ModelValidator.validateDsl(dslContent);
} catch (DslErrorsException e) {
// Handle validation errors
System.err.println("DSL errors: " + e.getErrors());
}
Alternative: Use CLI for conversion
# Convert DSL to JSON using the FGA CLI
fga model transform --input model.fga --output model.json
Then load the JSON file as shown above.
Check Permission
import dev.openfga.sdk.api.client.model.ClientCheckRequest;
var request = new ClientCheckRequest()
.user("user:anne")
.relation("viewer")
._object("document:roadmap");
var response = fgaClient.check(request).get();
// response.getAllowed() returns true/false
Batch Check
import dev.openfga.sdk.api.client.model.ClientBatchCheckRequest;
import dev.openfga.sdk.api.client.model.ClientBatchCheckItem;
var request = new ClientBatchCheckRequest().checks(
List.of(
new ClientBatchCheckItem()
.user("user:anne")
.relation("viewer")
._object("document:roadmap")
.correlationId("check-1"),
new ClientBatchCheckItem()
.user("user:bob")
.relation("editor")
._object("document:budget")
.correlationId("check-2")));
var options = new ClientBatchCheckOptions()
.maxParallelRequests(5)
.maxBatchSize(20);
var response = fgaClient.batchCheck(request, options).get();
Write Tuples
import dev.openfga.sdk.api.client.model.ClientWriteRequest;
import dev.openfga.sdk.api.model.TupleKey;
var request = new ClientWriteRequest()
.writes(List.of(
new TupleKey()
.user("user:anne")
.relation("viewer")
._object("document:roadmap")))
.deletes(List.of(
new TupleKey()
.user("user:bob")
.relation("editor")
._object("document:budget")));
var response = fgaClient.write(request).get();
List Objects
import dev.openfga.sdk.api.client.model.ClientListObjectsRequest;
var request = new ClientListObjectsRequest()
.user("user:anne")
.relation("viewer")
.type("document");
var response = fgaClient.listObjects(request).get();
// response.getObjects() returns accessible document IDs
List Relations
import dev.openfga.sdk.api.client.model.ClientListRelationsRequest;
var request = new ClientListRelationsRequest()
.user("user:anne")
._object("document:roadmap")
.relations(List.of("can_view", "can_edit", "can_delete"));
var response = fgaClient.listRelations(request).get();
// response.getRelations() returns applicable relations
List Users
import dev.openfga.sdk.api.client.model.ClientListUsersRequest;
import dev.openfga.sdk.api.model.FgaObject;
import dev.openfga.sdk.api.model.UserTypeFilter;
var userFilters = new ArrayList<UserTypeFilter>() {{
add(new UserTypeFilter().type("user"));
}};
var request = new ClientListUsersRequest()
._object(new FgaObject().type("document").id("roadmap"))
.relation("can_read")
.userFilters(userFilters);
var response = fgaClient.listUsers(request).get();
// response.getUsers() returns matching users
Read Tuples
import dev.openfga.sdk.api.client.model.ClientReadRequest;
var request = new ClientReadRequest()
.user("user:anne")
.relation("viewer")
._object("document:roadmap");
var response = fgaClient.read(request).get();
Non-Transaction Write Mode
var options = new ClientWriteOptions()
.disableTransactions(true)
.transactionChunkSize(100);
var response = fgaClient.write(request, options).get();
Handle Write Conflicts
import dev.openfga.sdk.api.model.WriteRequestWrites;
import dev.openfga.sdk.api.model.WriteRequestDeletes;
var options = new ClientWriteOptions()
.onDuplicate(WriteRequestWrites.OnDuplicateEnum.IGNORE)
.onMissing(WriteRequestDeletes.OnMissingEnum.IGNORE);
var response = fgaClient.write(request, options).get();
Contextual Tuples
var request = new ClientCheckRequest()
.user("user:anne")
.relation("viewer")
._object("document:roadmap")
.contextualTuples(List.of(
new ClientTupleKey()
.user("user:anne")
.relation("editor")
._object("document:roadmap")));
var response = fgaClient.check(request).get();
Retry Configuration
var config = new ClientConfiguration()
.apiUrl("http://localhost:8080")
.maxRetries(3)
.minimumRetryDelay(Duration.ofMillis(250));
var fgaClient = new OpenFgaClient(config);
Best Practices
- Initialize once: Create
OpenFgaClientonce and reuse throughout your application - Async handling: Use
.get()to block or.thenApply()for async - Object naming: Use
._object()(with underscore) for object parameter - Retry behavior: SDK auto-retries on 429 and 5xx errors (up to 3 times)
- Java version: Requires Java 11+
7.5 .NET SDK
Impact: HIGH (client implementation for .NET)
The OpenFga.Sdk package provides the official OpenFGA client for .NET applications.
Installation
dotnet add package OpenFga.Sdk
Supported frameworks: net8.0, net9.0, netstandard2.0, net48
Client Initialization
Basic setup:
using OpenFga.Sdk.Client;
using OpenFga.Sdk.Configuration;
var configuration = new ClientConfiguration() {
ApiUrl = "http://localhost:8080",
StoreId = Environment.GetEnvironmentVariable("FGA_STORE_ID"),
AuthorizationModelId = Environment.GetEnvironmentVariable("FGA_MODEL_ID"),
};
var fgaClient = new OpenFgaClient(configuration);
With API Token:
using OpenFga.Sdk.Configuration;
var configuration = new ClientConfiguration() {
ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL"),
StoreId = Environment.GetEnvironmentVariable("FGA_STORE_ID"),
Credentials = new Credentials() {
Method = CredentialsMethod.ApiToken,
Config = new CredentialsConfig() {
ApiToken = Environment.GetEnvironmentVariable("FGA_API_TOKEN"),
}
}
};
var fgaClient = new OpenFgaClient(configuration);
With Client Credentials (OAuth2):
var configuration = new ClientConfiguration() {
ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL"),
Credentials = new Credentials() {
Method = CredentialsMethod.ClientCredentials,
Config = new CredentialsConfig() {
ApiTokenIssuer = Environment.GetEnvironmentVariable("FGA_API_TOKEN_ISSUER"),
ApiAudience = Environment.GetEnvironmentVariable("FGA_API_AUDIENCE"),
ClientId = Environment.GetEnvironmentVariable("FGA_CLIENT_ID"),
ClientSecret = Environment.GetEnvironmentVariable("FGA_CLIENT_SECRET"),
}
}
};
var fgaClient = new OpenFgaClient(configuration);
Load Authorization Model from File
From JSON file:
using System.Text.Json;
using OpenFga.Sdk.Model;
// Read and parse JSON file
var jsonContent = await File.ReadAllTextAsync("model.json");
var modelJson = JsonSerializer.Deserialize<WriteAuthorizationModelRequest>(jsonContent);
var response = await fgaClient.WriteAuthorizationModel(modelJson);
// response.AuthorizationModelId contains the new model ID
From DSL (.fga) file:
The .NET SDK does not include a built-in DSL parser. Convert DSL files to JSON using the OpenFGA CLI, then load the JSON file.
# Convert DSL to JSON using the FGA CLI
fga model transform --input model.fga --output model.json
Then load the JSON file as shown above.
Check Permission
using OpenFga.Sdk.Client.Model;
var body = new ClientCheckRequest {
User = "user:anne",
Relation = "viewer",
Object = "document:roadmap"
};
var response = await fgaClient.Check(body);
// response.Allowed = true/false
Batch Check
var options = new ClientBatchCheckOptions {
MaxParallelRequests = 5,
MaxBatchSize = 20,
};
var body = new ClientBatchCheckRequest {
Checks = new List<ClientBatchCheckItem>() {
new() {
User = "user:anne",
Relation = "viewer",
Object = "document:roadmap",
CorrelationId = "check-1",
},
new() {
User = "user:bob",
Relation = "editor",
Object = "document:budget",
CorrelationId = "check-2",
}
}
};
var response = await fgaClient.BatchCheck(body, options);
Write Tuples
var body = new ClientWriteRequest() {
Writes = new List<ClientTupleKey> {
new() {
User = "user:anne",
Relation = "viewer",
Object = "document:roadmap",
}
},
Deletes = new List<ClientTupleKeyWithoutCondition> {
new() {
User = "user:bob",
Relation = "editor",
Object = "document:budget",
}
},
};
var response = await fgaClient.Write(body);
List Objects
var body = new ClientListObjectsRequest {
User = "user:anne",
Relation = "viewer",
Type = "document",
};
var response = await fgaClient.ListObjects(body);
// response.Objects contains accessible document IDs
Streamed List Objects
var options = new ClientListObjectsOptions {
Consistency = ConsistencyPreference.HIGHERCONSISTENCY
};
var objects = new List<string>();
await foreach (var response in fgaClient.StreamedListObjects(
new ClientListObjectsRequest {
User = "user:anne",
Relation = "can_read",
Type = "document"
},
options)) {
objects.Add(response.Object);
}
List Relations
var body = new ClientListRelationsRequest() {
User = "user:anne",
Object = "document:roadmap",
Relations = new List<string> {"can_view", "can_edit", "can_delete"},
};
var response = await fgaClient.ListRelations(body);
// response.Relations contains applicable relations
List Users
using OpenFga.Sdk.Model;
var body = new ClientListUsersRequest() {
Object = new FgaObject() {
Type = "document",
Id = "roadmap"
},
Relation = "can_read",
UserFilters = new List<UserTypeFilter> {
new() { Type = "user" }
},
};
var response = await fgaClient.ListUsers(body);
Read Tuples
var body = new ClientReadRequest() {
User = "user:anne",
Relation = "viewer",
Object = "document:roadmap",
};
var response = await fgaClient.Read(body);
Non-Transaction Write Mode
var options = new ClientWriteOptions {
Transaction = new TransactionOptions() {
Disable = true,
MaxParallelRequests = 5,
MaxPerChunk = 100,
}
};
var response = await fgaClient.Write(body, options);
Handle Write Conflicts
var options = new ClientWriteOptions {
Conflict = new ConflictOptions {
OnDuplicateWrites = OnDuplicateWrites.Ignore,
OnMissingDeletes = OnMissingDeletes.Ignore
}
};
var response = await fgaClient.Write(body, options);
Contextual Tuples
var body = new ClientCheckRequest {
User = "user:anne",
Relation = "viewer",
Object = "document:roadmap",
ContextualTuples = new List<ClientTupleKey> {
new() {
User = "user:anne",
Relation = "editor",
Object = "document:roadmap",
},
},
};
var response = await fgaClient.Check(body);
Retry Configuration
var configuration = new ClientConfiguration() {
ApiUrl = "http://localhost:8080",
RetryParams = new RetryParams() {
MaxRetry = 3,
MinWaitInMs = 250
}
};
var fgaClient = new OpenFgaClient(configuration);
Per-Request Headers
var options = new ClientCheckOptions {
Headers = new Dictionary<string, string> {
{ "X-Request-ID", "123e4567-e89b-12d3-a456-426614174000" }
}
};
var response = await fgaClient.Check(body, options);
Best Practices
- Initialize once: Create
OpenFgaClientonce and reuse throughout your application - Async/await: All methods are async - use
awaitproperly - Streaming: Use
StreamedListObjectswithawait foreachfor large result sets - Retry behavior: SDK auto-retries on 429 and 5xx errors (up to 3 times)
- Retry-After: SDK respects the
Retry-Afterheader with exponential backoff
8. Workflow
Impact: CRITICAL
Essential workflow practices for working with OpenFGA models.
8.1 Always Validate Models
Impact: CRITICAL (mandatory workflow step)
CRITICAL: After creating or modifying any .fga or .fga.yaml file, you MUST immediately run tests to validate the model. Never deliver an untested model.
Incorrect: Delivering Untested Model
1. Create/modify .fga model
2. Create/modify .fga.yaml tests
3. Deliver to user ❌ WRONG
The model may have syntax errors, logical errors, or test assertions that don't match actual behavior.
Correct: Validate Before Delivery
1. Create/modify .fga model
2. Create/modify .fga.yaml tests
3. Run: fga model test --tests <file>.fga.yaml ✓
4. If tests fail: fix model or tests, go to step 3
5. Deliver to user with test results ✓
Command
fga model test --tests <filename>.fga.yaml
Why This Matters
- Syntax errors: The DSL parser will catch invalid syntax
- Logical errors: Tests verify permissions work as intended
- Inheritance bugs: Complex
fromrelationships may not behave as expected - Missing tuples: Tests ensure all required tuples exist for assertions
Example Workflow
# After creating notion.fga and notion.fga.yaml
$ fga model test --tests notion.fga.yaml
# Expected output for passing tests:
# Test Summary #
Tests 14/14 passing
Checks 123/123 passing
ListObjects 3/3 passing
ListUsers 1/1 passing
# If tests fail, fix the issues and re-run until all pass
Non-Negotiable
This step is not optional. An untested authorization model may:
- Grant access to users who shouldn't have it
- Deny access to users who should have it
- Cause security vulnerabilities in production
Always run tests. Always report results to the user.