FGA Cache
The Descope AuthZ Cache is a high-performance authorization cache service that accelerates Fine-Grained Authorization (FGA) checks by caching authorization data locally within your cluster.
By deploying this service alongside your application, you can significantly reduce latency for authorization checks and improve overall application performance.
Overview
The AuthZ Cache service applies to select authorization operations that use Descope's FGA service (/v1/mgmt/fga/* endpoints).
The cache service acts as a local cache layer between your application and Descope's authorization services. It automatically syncs with remote authorization data and provides fast, local access to relationship and permission information needed for FGA checks.
How It Works
The AuthZ Cache service:
- Connects to Descope: Uses your management key to authenticate and fetch authorization data
- Caches Locally: Stores direct and indirect relations in local memory for fast access using LRU (Least Recently Used) eviction when caches reach their configured size limits
- Syncs Automatically: Periodically polls Descope's services (based on
AUTHZCACHE_REMOTE_POLLING_INTERVAL_IN_MILLIS) to keep the cache up-to-date - Serves Requests: Responds to FGA check requests from your application SDKs with cached data
When your application performs any FGA operations (ReBAC relation checks, ABAC attribute evaluations, schema queries, etc.), the SDK first queries the local cache service.
If the data is available and fresh, it's returned immediately. If not, or if the cache needs to refresh, it falls back to querying Descope's authz service directly.
Cache Behavior
The cache uses a sophisticated two-tier caching strategy:
- Direct Relations Cache: Stores direct relationships (e.g.,
user1directly ownsfile1) - Indirect Relations Cache: Stores indirect relationships (e.g.,
user1ownsfile1through group membership)
Cache Lookup Order:
- Check direct relations cache first
- If not found, check indirect relations cache
- If still not found, query Descope SDK and update cache with result
Cache Invalidation:
- Schema changes: Purges all caches (direct + indirect) to ensure consistency
- Relation additions/deletions: Updates direct cache incrementally, purges indirect cache (since indirect relations may have changed)
- Remote polling: Detects changes via polling and removes affected resources/targets from cache
- Polling errors: By default, purges all caches for safety to prevent serving stale data. If
AUTHZCACHE_PURGE_COOLDOWN_WINDOW_IN_MINUTESis set to a positive value, the cache waits that long after the first error before purging. It keeps serving existing data, stale or not, during the wait, and cancels the purge if a poll succeeds before the window closes
Negative Caching: Both allowed (true) and denied (false) authorization results are cached to avoid repeated remote queries for the same checks.
Write Operations: When you create or delete relations through the cache service, the cache is updated immediately - you don't need to wait for the next polling cycle. This ensures write operations are immediately reflected in subsequent read operations.
Lookup Cache
In addition to the relation check cache, the AuthZ Cache also caches responses from lookup queries:
- WhoCanAccess (
/v1/mgmt/authz/re/who) — returns all targets that have a given relation to a resource - WhatCanTargetAccess (
/v1/mgmt/authz/re/targetall) — returns all resources a given target can access
The lookup cache has a configurable TTL and size limit. Before returning a cached result, it re-verifies every candidate against the relation check cache and drops any that no longer hold.
Tune or disable the lookup cache independently of the relation check cache with the AUTHZCACHE_LOOKUP_CACHE_* environment variables. When disabled, lookup requests pass straight through to the Descope backend, so you can turn this cache off and still route lookup queries through the authzcache container.
Lookup Cache Freshness
Between refreshes, a cached lookup result can shrink but never grow. Re-verification removes candidates, and only a fresh backend query adds them.
| Change | When it reaches lookup results |
|---|---|
| Revocation written through the cache container | Immediately |
| Revocation written directly to the backend | On the next successful poll |
| New grant, either path | When the cached result's TTL expires |
Remote Polling
The service automatically starts polling for remote changes when a project cache is created. Polling behavior:
- Polls Descope's
GetModifiedAPI for changes since the last poll time - On schema changes → purges all caches
- On relation changes → removes affected resources/targets from direct cache, purges indirect cache
- If no cached relations exist → skips remote call but invalidates schema cache to detect schema changes
- On polling errors → purges all caches for safety, unless
AUTHZCACHE_PURGE_COOLDOWN_WINDOW_IN_MINUTESdelays the purge (see Cache Invalidation above)
The polling interval is configurable via AUTHZCACHE_REMOTE_POLLING_INTERVAL_IN_MILLIS (minimum 15,000ms).
Run with Docker
The management key must have proper FGA read/write permissions for the cache to function correctly.
In order to deploy the FGA Cache service using Docker, you can use the following command:
docker run -d \
--name authzcache \
-p 8189:8189 \
-e HTTP_HOST=0.0.0.0 \
-e DESCOPE_MANAGEMENT_KEY=your_management_key_here \
descope/authzcache:latestThe service exposes the following port 8189 for HTTP REST API. The service uses gRPC internally and exposes HTTP REST API via gRPC-Gateway.
Configuration
Required Environment Variables
DESCOPE_MANAGEMENT_KEY- Your Descope management key for authentication. This key must have FGA read/write permissions. The management key is used by the cache service to authenticate with Descope's services.
Optional Environment Variables
DESCOPE_BASE_URL- Custom Descope base URL (default: production Descope service)CONTAINER_HTTP_PORT- HTTP gateway port (default:8189)AUTHZCACHE_SDK_DEBUG_LOG- Enable debug logging of the internally used Descope SDK (TRUE/FALSE, default:FALSE)AUTHZCACHE_DIRECT_RELATION_CACHE_SIZE_PER_PROJECT- Direct relation cache size per project (default:1,000,000). Note: This is per project - if you have multiple projects, each maintains its own cache of this size.AUTHZCACHE_INDIRECT_RELATION_CACHE_SIZE_PER_PROJECT- Indirect relation cache size per project (default:1,000,000). Note: This is per project - if you have multiple projects, each maintains its own cache of this size.AUTHZCACHE_REMOTE_POLLING_INTERVAL_IN_MILLIS- Remote polling interval in milliseconds (default:15,000). Minimum value is 15,000ms (15 seconds) - any value below this will be automatically increased to 15,000ms.AUTHZCACHE_PURGE_COOLDOWN_WINDOW_IN_MINUTES- Cooldown window in minutes before purging the cache on a remote polling error (default:0, meaning purge immediately). Set it to a positive value and the cache waits that long after the first error before purging, serving existing (possibly stale) data in the meantime. A successful poll during the window cancels the purge.AUTHZCACHE_LOOKUP_CACHE_ENABLED- Enable the lookup cache forWhoCanAccess/WhatCanTargetAccessqueries (TRUE/FALSE, default:TRUE)AUTHZCACHE_LOOKUP_CACHE_SIZE_PER_PROJECT- Max number of lookup cache entries per project (default:10,000)AUTHZCACHE_LOOKUP_CACHE_TTL_IN_SECONDS- TTL in seconds for lookup cache entries (default:60, minimum:1)AUTHZCACHE_LOOKUP_CACHE_MAX_RESULT_SIZE- Skip caching lookup results larger than this size (default:1,000)AUTHZCACHE_METRICS_REPORT_ENABLED- Enable cache metrics reporting to Descope backend for observability (TRUE/FALSE, default:TRUE).AUTHZCACHE_METRICS_REPORT_INTERVAL_IN_SECONDS- Interval in seconds for reporting aggregated cache metrics to Descope backend (default:60, minimum:10). Only applies when metrics reporting is enabled.AUTHZCACHE_HTTP_WRITE_TIMEOUT_IN_SECONDS- Max seconds the HTTP gateway spends producing a response before closing the connection (default:30). Takes precedence over the genericHTTP_GATEWAY_WRITE_TIMEOUT, which is still honored when this is unset.
In Your Application
To have the Descope SDK use this cache container for accelerating FGA operations (including ReBAC and ABAC checks), pass its URL via the FGACacheURL configuration field when initializing your Descope SDK client. Point the URL to the running container/service inside your local environment or cluster.
Once configured, all authorization operations that use the FGA service will benefit from the local cache.
Note
The FGA Cache accelerates operations on /v1/mgmt/fga/* endpoints. Additionally, lookup queries — WhoCanAccess (/v1/mgmt/authz/re/who) and WhatCanTargetAccess (/v1/mgmt/authz/re/targetall) — are also cached via the lookup cache. Cached lookup candidates are always re-verified against the fresh check cache before being returned.
URL Configuration
- Local Docker container:
http://localhost:8189(or the mapped host/port you selected) - Kubernetes service: Use the internal service DNS name, e.g.,
http://authzcache.default.svc.cluster.local:8189
Ensure the container is reachable from where your code runs. Check network policies, firewall rules, and service mesh settings to allow communication between your application and the cache service.
SDK Examples
Note
The cache proxy requires both fgaCacheUrl and managementKey. Without managementKey, requests use the standard Descope API instead.
import DescopeClient from '@descope/node-sdk';
// Configuration constants
const YourProjectID = 'your-project-id';
const YourFGAReadWriteApprovedMGMTKey = 'your-management-key'; // must have proper FGA permissions
const URLToThisContainer = 'http://localhost:8189'; // or cluster service URL
// Initialize the Descope SDK client with the AuthZ cache URL
const descopeClient = DescopeClient({
projectId: YourProjectID,
managementKey: YourFGAReadWriteApprovedMGMTKey,
fgaCacheUrl: URLToThisContainer,
});
// Continue with your application logic...When you configure fgaCacheUrl, these FGA methods route through the cache proxy instead of the default Descope API: saveSchema, createRelations, deleteRelations, and check.
If the cache proxy is unreachable or returns an error, the SDK falls back to the standard Descope API. loadResourcesDetails and saveResourcesDetails can only use the standard Descope API endpoints.
package main
import (
"context"
"github.com/descope/go-sdk/descope/client"
)
const (
YourProjectID = "__ProjectID__"
YourFGAReadWriteApprovedMGMTKey = "your-management-key" // must have proper FGA permissions
URLToThisContainer = "http://localhost:8189" // or cluster service URL
)
func main() {
ctx := context.Background()
// Initialize the Descope SDK client with the AuthZ cache URL
err := client.InitDescopeSDKClient(ctx, &client.DescopeSDKClientConfig{
ProjectID: YourProjectID,
MgmtKey: YourFGAReadWriteApprovedMGMTKey,
FGACacheURL: URLToThisContainer,
})
if err != nil {
panic(err)
}
// Continue with your application logic...
}from descope import DescopeClient
# Configuration constants
YOUR_PROJECT_ID = "__ProjectID__"
YOUR_FGA_READ_WRITE_APPROVED_MGMT_KEY = "your-management-key" # must have proper FGA permissions
URL_TO_THIS_CONTAINER = "http://localhost:8189" # or cluster service URL
# Initialize the Descope SDK client with the AuthZ cache URL
try:
descope_client = DescopeClient(
project_id=YOUR_PROJECT_ID,
management_key=YOUR_FGA_READ_WRITE_APPROVED_MGMT_KEY,
fga_cache_url=URL_TO_THIS_CONTAINER,
)
except Exception as error:
raise error
# Continue with your application logic...API Endpoints
The service supports multiple projects simultaneously (multi-tenant). Each project maintains its own isolated cache instance with separate direct/indirect relation caches and independent remote polling.
The service exposes REST API endpoints for managing FGA schemas, relations, and performing authorization checks.
All endpoints require authentication with a bearer token via the Authorization header in the following format: Bearer <Descope Project ID>:<Management Key>
Create or update the FGA schema for your project. The schema defines the authorization model including namespaces, relation definitions, and permissions.
Request Body:
{
"dsl": "string"
}dsl(string, required) - The FGA schema in DSL (Domain-Specific Language) format
Example Request:
curl -X POST "http://localhost:8189/v1/mgmt/fga/schema" \
-H "Authorization: Bearer <Project ID>:<Management Key>" \
-H "Content-Type: application/json" \
-d '{
"dsl": "model AuthZ 1.0\ntype user\ntype doc\n relations\n define owner: [user]\n define viewer: [user]"
}'When relations are created through the cache service, the cache is updated immediately. You don't need to wait for the next polling cycle for these changes to be reflected.
Create FGA relations (tuples) that define relationships between resources and targets. Relations are the actual data that represents who has access to what resources.
Request Body:
{
"tuples": [
{
"resource": "string",
"resourceType": "string",
"relation": "string",
"target": "string",
"targetType": "string"
}
]
}tuples(array, required) - Array of relation tuples to createresource(string, required) - The resource identifier (e.g.,"doc-123")resourceType(string, required) - The type of resource (e.g.,"doc")relation(string, required) - The relation definition name (e.g.,"owner","viewer")target(string, required) - The target identifier, usually a user ID (e.g.,"user-456")targetType(string, required) - The type of target (e.g.,"user")
Example Request:
curl -X POST "http://localhost:8189/v1/mgmt/fga/relations" \
-H "Authorization: Bearer <Project ID>:<Management Key>" \
-H "Content-Type: application/json" \
-d '{
"tuples": [
{
"resource": "doc-123",
"resourceType": "doc",
"relation": "owner",
"target": "user-456",
"targetType": "user"
},
{
"resource": "doc-123",
"resourceType": "doc",
"relation": "viewer",
"target": "user-789",
"targetType": "user"
}
]
}'When relations are deleted through the cache service, the cache is updated immediately. The direct cache is updated incrementally, and the indirect cache is purged to ensure consistency.
Delete FGA relations (tuples) from your authorization model. This removes the specified relationships between resources and targets.
Request Body:
{
"tuples": [
{
"resource": "string",
"resourceType": "string",
"relation": "string",
"target": "string",
"targetType": "string"
}
]
}tuples(array, required) - Array of relation tuples to deleteresource(string, required) - The resource identifierresourceType(string, required) - The type of resourcerelation(string, required) - The relation definition nametarget(string, required) - The target identifiertargetType(string, required) - The type of target
Example Request:
curl -X POST "http://localhost:8189/v1/mgmt/fga/relations/delete" \
-H "Authorization: Bearer <Project ID>:<Management Key>" \
-H "Content-Type: application/json" \
-d '{
"tuples": [
{
"resource": "doc-123",
"resourceType": "doc",
"relation": "viewer",
"target": "user-789",
"targetType": "user"
}
]
}'This endpoint is optimized for performance and uses the local cache to serve authorization checks. Both allowed (true) and denied (false) results are cached to avoid repeated remote queries for the same checks.
Check if the given relations are allowed. This is the main endpoint for performing authorization checks. It determines whether a target (typically a user) has a specific relation to a resource.
Request Body:
{
"tuples": [
{
"resource": "string",
"resourceType": "string",
"relation": "string",
"target": "string",
"targetType": "string"
}
],
"computePaths": false
}tuples(array, required) - Array of relation tuples to checkresource(string, required) - The resource identifier to check access forresourceType(string, required) - The type of resourcerelation(string, required) - The relation definition to check (e.g.,"owner","viewer")target(string, required) - The target identifier (usually a user ID) to check access fortargetType(string, required) - The type of target (usually"user")
computePaths(boolean, optional) - Iftrue, includes the full path of intermediate relations in the response. Default:false
Response:
{
"tuples": [
{
"allowed": true,
"tuple": {
"resource": "string",
"resourceType": "string",
"relation": "string",
"target": "string",
"targetType": "string"
},
"info": {
"direct": true,
"path": {
"steps": [
{
"stepType": 0,
"tuple": {
"resource": "string",
"resourceType": "string",
"relation": "string",
"target": "string",
"targetType": "string"
},
"permission": "string",
"subPaths": []
}
]
}
}
}
]
}tuples(array) - Array of check results, one for each input tupleallowed(boolean) -trueif the relation is allowed,falseotherwisetuple(object) - The original tuple that was checkedinfo(object, optional) - Additional information about the checkdirect(boolean) -trueif the relation is direct (can only be changed by creating/deleting relations involving the resource or target),falseif indirectpath(object, optional) - IfcomputePathswastrueand the check succeeded, contains the full path of intermediate relations between the target and resource
Example Request:
curl -X POST "http://localhost:8189/v1/mgmt/fga/check" \
-H "Authorization: Bearer <Project ID>:<Management Key>" \
-H "Content-Type: application/json" \
-d '{
"tuples": [
{
"resource": "doc-123",
"resourceType": "doc",
"relation": "viewer",
"target": "user-456",
"targetType": "user"
}
],
"computePaths": false
}'Example Response:
{
"tuples": [
{
"allowed": true,
"tuple": {
"resource": "doc-123",
"resourceType": "doc",
"relation": "viewer",
"target": "user-456",
"targetType": "user"
},
"info": {
"direct": true
}
}
]
}This endpoint is served by the lookup cache. Cached candidates are always re-verified against the check cache before being returned, so revocations written through the cache container, or already picked up by a successful poll, are never served.
A newly granted relation, though, may not appear until the cached result's TTL expires; see Lookup Cache Freshness for details, including the staleness bounds that apply to revocations made directly against the backend.
Find all targets that have a given relation to a resource.
Request Body:
{
"resource": "string",
"relationDefinition": "string",
"namespace": "string"
}resource(string, required) - The resource identifier to query (e.g.,"doc-123")relationDefinition(string, required) - The relation definition to query (e.g.,"viewer")namespace(string, required) - The namespace of the relation definition (e.g.,"doc")
Response:
{
"targets": ["string"]
}targets(array of strings) - The identifiers of all targets that have the given relation to the resource
Example Request:
curl -X POST "http://localhost:8189/v1/mgmt/authz/re/who" \
-H "Authorization: Bearer <Project ID>:<Management Key>" \
-H "Content-Type: application/json" \
-d '{
"resource": "doc-123",
"relationDefinition": "viewer",
"namespace": "doc"
}'Example Response:
{
"targets": ["user-456", "user-789"]
}This endpoint is served by the lookup cache. Cached candidates are always re-verified against the check cache before being returned, so revocations written through the cache container, or already picked up by a successful poll, are never served. A newly granted relation, though, may not appear until the cached result's TTL expires; see Lookup Cache Freshness for details, including the staleness bounds that apply to revocations made directly against the backend.
Find all resources (and the relations through which they're reachable) that a given target can access.
Request Body:
{
"target": "string"
}target(string, required) - The target identifier to query, usually a user ID (e.g.,"user-456")
Response:
{
"relations": [
{
"resource": "string",
"relationDefinition": "string",
"namespace": "string",
"target": "string",
"targetNamespace": "string",
"targetSetResource": "string",
"targetSetRelationDefinition": "string",
"targetSetRelationDefinitionNamespace": "string"
}
]
}relations(array) - All relations through which the target can reach a resourceresource(string) - The resource the relation is defined onrelationDefinition(string) - The name of the relation definitionnamespace(string) - The namespace of the relation definitiontarget(string) - The target for the relationtargetNamespace(string) - The namespace of the targettargetSetResource,targetSetRelationDefinition,targetSetRelationDefinitionNamespace(string, optional) - Present instead oftarget/targetNamespacewhen the relation resolves through a target set (anyone who has another relation), identifying that other relation
Example Request:
curl -X POST "http://localhost:8189/v1/mgmt/authz/re/targetall" \
-H "Authorization: Bearer <Project ID>:<Management Key>" \
-H "Content-Type: application/json" \
-d '{
"target": "user-456"
}'Example Response:
{
"relations": [
{
"resource": "doc-123",
"relationDefinition": "viewer",
"namespace": "doc",
"target": "user-456",
"targetNamespace": "user"
}
]
}The service exposes health check endpoints for container orchestration:
HTTP: GET http://localhost:8189/healthz
This endpoint can be used by Kubernetes liveness and readiness probes, or other orchestration tools to monitor the service health.
Example Request:
curl -X GET "http://localhost:8189/healthz"Deployment Considerations
-
The cache automatically syncs with remote authorization data based on the polling interval environment variable
-
Ensure the container is reachable from where your code runs (network policy / firewall / service mesh settings)
-
The cache size can be configured per project to match your authorization data volume. Each project maintains its own isolated cache.
-
Caches use LRU (Least Recently Used) eviction - when a cache reaches its size limit, the least recently used entries are evicted to make room for new entries.
-
For production deployments, consider running multiple cache instances behind a load balancer for high availability
-
The service supports multiple projects simultaneously - each project gets its own cache instance with isolated data and independent polling