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:

  1. Connects to Descope: Uses your management key to authenticate and fetch authorization data
  2. 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
  3. Syncs Automatically: Periodically polls Descope's services (based on AUTHZCACHE_REMOTE_POLLING_INTERVAL_IN_MILLIS) to keep the cache up-to-date
  4. 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., user1 directly owns file1)
  • Indirect Relations Cache: Stores indirect relationships (e.g., user1 owns file1 through group membership)

Cache Lookup Order:

  1. Check direct relations cache first
  2. If not found, check indirect relations cache
  3. 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_MINUTES is 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.

ChangeWhen it reaches lookup results
Revocation written through the cache containerImmediately
Revocation written directly to the backendOn the next successful poll
New grant, either pathWhen 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 GetModified API 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_MINUTES delays 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:

Terminal
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:latest

The 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 for WhoCanAccess/WhatCanTargetAccess queries (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 generic HTTP_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.

app.ts
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.

app.go
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...
}
app.py
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"
}

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 create
    • resource (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 delete
    • resource (string, required) - The resource identifier
    • resourceType (string, required) - The type of resource
    • relation (string, required) - The relation definition name
    • target (string, required) - The target identifier
    • targetType (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 check
    • resource (string, required) - The resource identifier to check access for
    • resourceType (string, required) - The type of resource
    • relation (string, required) - The relation definition to check (e.g., "owner", "viewer")
    • target (string, required) - The target identifier (usually a user ID) to check access for
    • targetType (string, required) - The type of target (usually "user")
  • computePaths (boolean, optional) - If true, 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 tuple
    • allowed (boolean) - true if the relation is allowed, false otherwise
    • tuple (object) - The original tuple that was checked
    • info (object, optional) - Additional information about the check
      • direct (boolean) - true if the relation is direct (can only be changed by creating/deleting relations involving the resource or target), false if indirect
      • path (object, optional) - If computePaths was true and 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 resource
    • resource (string) - The resource the relation is defined on
    • relationDefinition (string) - The name of the relation definition
    • namespace (string) - The namespace of the relation definition
    • target (string) - The target for the relation
    • targetNamespace (string) - The namespace of the target
    • targetSetResource, targetSetRelationDefinition, targetSetRelationDefinitionNamespace (string, optional) - Present instead of target/targetNamespace when 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

Was this helpful?

On this page