Skip to main content

Permission Cache

Multi-level permission caching with local, Redis, and OpenFGA fallback for high-performance authorization checks.

Overview

Based on pkg/v2/infrastructure/permission/cache/, the permission cache provides:

  • Three-Level Cache: Local (sync.Map) → Redis → OpenFGA
  • Distributed Consistency: Event-driven cache invalidation across instances
  • High Performance: Sub-millisecond local cache lookups
  • Cache Statistics: Hit/miss tracking for monitoring
  • Pattern-Based Invalidation: Wildcard pattern support for cache clearing

Architecture

Multi-Level Cache Strategy

Level 1: Local Cache (sync.Map)
├─ Fastest: In-memory, no network
├─ TTL: 1 second
└─ Scope: Single AppServer instance

Level 2: Redis Cache
├─ Fast: Distributed, network call
├─ TTL: 5 minutes
└─ Scope: All AppServer instances

Level 3: OpenFGA (Source of Truth)
├─ Slowest: Authorization service
├─ No TTL: Always authoritative
└─ Scope: Platform-wide

Cache Flow

Permission Check Request

Check Local Cache
├─ HIT → Return immediately
└─ MISS ↓
Check Redis Cache
├─ HIT → Store in Local, Return
└─ MISS ↓
Query OpenFGA
├─ Result → Store in Redis and Local
└─ Return

Implementation

Based on cache.go:

MultiLevelCache Structure

type MultiLevelCache struct {
local *LocalCache
redis *RedisCache
checker *authz.PermissionChecker
stats *Stats
logger telemetry.Logger
}

Configuration

type Config struct {
RedisAddr string
RedisPassword string
RedisDB int
EnableRedis bool // Allow disabling for testing
}

cache, err := NewMultiLevelCache(checker, config, logger)

Cache Operations

CheckRouteAccess

Check if user can access a route:

allowed, err := cache.CheckRouteAccess(ctx, authCtx, routeID)

Cache Key Format:

{subject}:accessible_by:route:{routeID}

Examples:

user:123:accessible_by:route:550e8400-e29b-41d4-a716-446655440000
app:todos-app:accessible_by:route:550e8400-e29b-41d4-a716-446655440000

CheckPermission

Check if user has a specific permission:

allowed, err := cache.CheckPermission(ctx, authCtx, "todos:read")

Cache Key Format:

{subject}:granted_to:permission:{scope}

Examples:

user:123:granted_to:permission:todos:read
app:todos-app:granted_to:permission:users:read

CheckHookAccess

Check if user can access a hook:

allowed, err := cache.CheckHookAccess(ctx, authCtx, hookID, "trigger")

CheckActivityAccess

Check if user can access an activity:

allowed, err := cache.CheckActivityAccess(ctx, authCtx, activityID, "execute")

Local Cache

Based on local.go:

LocalCache Structure

type LocalCache struct {
cache sync.Map // key: string, value: CacheEntry
}

type CacheEntry struct {
Value bool
ExpiresAt time.Time
}

Configuration

  • TTL: 1 second
  • Implementation: sync.Map for lock-free reads
  • Eviction: Lazy (on access) + periodic cleanup

Operations

// Get from local cache
result, found := local.Get(ctx, key)

// Set in local cache (1s TTL)
err := local.Set(ctx, key, allowed)

// Clear all entries
local.Clear()

// Delete by pattern
local.InvalidatePattern(pattern) // e.g., "user:123:*"

Redis Cache

Based on redis.go:

RedisCache Structure

type RedisCache struct {
client *redis.Client
ttl time.Duration // 5 minutes
}

Operations

// Get from Redis
result, found, err := redis.Get(ctx, key)

// Set in Redis (5min TTL)
err := redis.Set(ctx, key, allowed)

// Delete by pattern (SCAN + DEL)
err := redis.InvalidatePattern(ctx, "user:123:*")

Pattern Matching:

  • Uses Redis SCAN for pattern-based deletion
  • Supports wildcards: user:*, *:route:123
  • Atomic operations via pipelining

Cache Invalidation

Based on invalidation.go:

Event-Driven Invalidation

Subscribe to Invalidation Events:

eventBus.Subscribe("permission.invalidated", func(ctx context.Context, evt event.Event) error {
var payload event.PermissionInvalidatePayload
json.Unmarshal(evt.Payload(), &payload)

// Invalidate by subject
if payload.Subject != "" {
cache.Invalidate(ctx, payload.Subject)
}

// Invalidate by pattern
if payload.Pattern != "" {
cache.InvalidatePattern(ctx, payload.Pattern)
}

return nil
})

Invalidation Strategies

1. Subject-Based Invalidation:

// Invalidate all permissions for a user
cache.Invalidate(ctx, "user:123")

// Clears keys like:
// - user:123:accessible_by:route:*
// - user:123:granted_to:permission:*

2. Pattern-Based Invalidation:

// Invalidate all route access checks
cache.InvalidatePattern(ctx, "*:accessible_by:route:*")

// Invalidate specific app permissions
cache.InvalidatePattern(ctx, "app:todos-app:*")

3. Full Cache Clear:

// Clear entire cache (conservative approach)
cache.InvalidatePattern(ctx, "*")

Invalidation Triggers

App Uninstallation:

// Invalidate all permissions for uninstalled app
evt := event.NewPermissionInvalidateEvent("", fmt.Sprintf("app:%s:*", appName))
eventBus.Publish(ctx, evt)

Permission Changes:

// Invalidate specific user permissions
evt := event.NewPermissionInvalidateEvent(fmt.Sprintf("user:%s", userID), "")
eventBus.Publish(ctx, evt)

Cache Statistics

Based on stats.go:

Stats Structure

type Stats struct {
LocalHits atomic.Int64
RedisHits atomic.Int64
OpenFGAHits atomic.Int64
}

Metrics

stats := cache.GetStats()

fmt.Printf("Local Hits: %d\n", stats.LocalHits.Load())
fmt.Printf("Redis Hits: %d\n", stats.RedisHits.Load())
fmt.Printf("OpenFGA Hits: %d\n", stats.OpenFGAHits.Load())

// Calculate cache hit ratio
total := stats.LocalHits.Load() + stats.RedisHits.Load() + stats.OpenFGAHits.Load()
cacheHits := stats.LocalHits.Load() + stats.RedisHits.Load()
hitRatio := float64(cacheHits) / float64(total) * 100

Typical Ratios (production):

  • Local Hits: ~90-95%
  • Redis Hits: ~4-9%
  • OpenFGA Hits: ~1-5%

Performance Characteristics

Latency

Local Cache:

  • Latency: < 1ms
  • Throughput: Millions of ops/sec

Redis Cache:

  • Latency: 1-5ms (local network)
  • Throughput: Thousands of ops/sec

OpenFGA:

  • Latency: 10-50ms (depends on network and load)
  • Throughput: Hundreds of ops/sec

Cache Efficiency

Cache Hit Rate:

  • Target: > 95% combined (local + Redis)
  • Typical: 95-99% in production

Memory Usage:

  • Local: ~1-10MB per instance
  • Redis: ~100MB-1GB shared across instances

Best Practices

Caching Strategy

DO:

  • Use pattern-based invalidation for related permissions
  • Invalidate conservatively (clear entire cache if unsure)
  • Monitor cache hit ratios
  • Use short TTLs for frequently changing permissions

DON'T:

  • Cache negative results for too long
  • Assume cache is always consistent
  • Skip OpenFGA on cache hits (cache is optimization, not source of truth)

Invalidation

DO:

  • Publish invalidation events after permission changes
  • Use specific patterns when possible (more efficient)
  • Invalidate across all AppServer instances via events

DON'T:

  • Invalidate synchronously (use events for async invalidation)
  • Over-invalidate (causes unnecessary OpenFGA queries)
  • Under-invalidate (causes stale permission data)

Error Handling

DO:

  • Fall back to OpenFGA on cache errors
  • Log cache failures
  • Continue operation even if cache unavailable

DON'T:

  • Fail requests on cache errors
  • Silently ignore cache errors
  • Assume cache availability

Code References

ComponentFilePurpose
MultiLevelCachecache.goMain cache implementation
LocalCachelocal.goIn-memory cache
RedisCacheredis.goDistributed cache
Invalidationinvalidation.goCache invalidation logic
Statsstats.goHit/miss tracking