# ayz-auth v2.3.0 Caching Behavior Guide

**For:** Frontend and backend developers understanding cache behavior
**Created:** 2025-11-18
**Version:** 2.3.0

---

## Executive Summary

**Good news!** In v2.3.0, team context (`available_teams[]`) is **ALWAYS loaded fresh from MongoDB** on every request. Your frontend **does NOT need to invalidate cache** when team memberships change.

### What's Cached vs Fresh

| Data | Caching Behavior | TTL | Cache Invalidation Needed? |
|------|------------------|-----|---------------------------|
| **Session token** | Cached | Until session expires | ❌ No (auto-expires) |
| **Organization entitlements** | Cached | 1 hour | ⚠️ Only if entitlements change |
| **User ID (mongo_user_id)** | Cached | 1 hour | ❌ No (never changes) |
| **Team context (available_teams)** | **NOT cached** | N/A - always fresh | ✅ No invalidation needed! |

---

## Detailed Caching Breakdown

### 1. Session Token Cache ✅ Auto-Managed

**What's cached:**
```python
{
    "member_id": "member-live-123",
    "session_id": "session-live-456",
    "organization_id": "org-live-789",
    "session_expires_at": "2025-11-18T12:00:00Z",
    "entitlements": ["foresight", "byod"],
    # ... other session data
    # NOTE: available_teams[] NOT included in cache
}
```

**Cache key:**
```
ayz_auth:token:<sha256_hash_of_token>
```

**TTL:** Matches session expiration (typically 30 minutes to 24 hours)

**Invalidation:** Automatic when session expires

**Frontend action needed:** ❌ None - handled automatically

---

### 2. Organization Entitlements Cache ⚠️ Rarely Needs Invalidation

**What's cached:**
```python
{
    "entitlements": ["foresight", "byod"],
    "subscription_tier": "premium",
    "subscription_limits": {"max_projects": 50},
    "mongo_organization_id": "507f1f77bcf86cd799439011"
}
```

**Cache key:**
```
ayz_auth:entitlements:org:<stytch_org_id>
```

**Example:**
```
ayz_auth:entitlements:org:organization-live-abc123
```

**TTL:** 1 hour (3600 seconds)

**Invalidation needed when:**
- Organization upgrades/downgrades subscription
- Entitlements are added/removed for the organization
- Subscription limits change

**Invalidation needed for:**
- ⚠️ **Rare** - only when org-level changes occur (admin actions)
- Frequency: Maybe once per week/month (subscription changes)

**How to invalidate:**
```bash
# Redis CLI
redis-cli DEL "ayz_auth:entitlements:org:organization-live-abc123"

# Or in Python (if you have backend admin endpoint)
import redis
r = redis.Redis()
r.delete("ayz_auth:entitlements:org:organization-live-abc123")
```

**Frontend action needed:** ❌ None - this is admin-level data that changes rarely

---

### 3. User ID Cache ✅ Never Needs Invalidation

**What's cached:**
```python
# Only the MongoDB user ID string
"507f1f77bcf86cd799439011"
```

**Cache key:**
```
ayz_auth:user_context:<stytch_member_id>:org:<stytch_org_id>
```

**Example:**
```
ayz_auth:user_context:member-live-xyz789:org:organization-live-abc123
```

**TTL:** 1 hour (3600 seconds)

**Invalidation needed when:** Never - user IDs don't change

**Frontend action needed:** ❌ None

---

### 4. Team Context (available_teams[]) ✅ ALWAYS FRESH

**What's loaded:**
```python
{
    "available_teams": [
        {
            "id": "team-123",
            "name": "Marketing Team",
            "organization_id": "org-456",
            "role": "admin",
            "permissions": ["read", "write"]
        },
        # ... more teams
    ]
}
```

**Caching:** ❌ **NOT CACHED** - loaded fresh from MongoDB on every request

**Why not cached:**
- Team memberships can change frequently (users added/removed)
- Roles can change (promotions, demotions)
- Permissions can change (role updates)
- Cache invalidation would be complex (who invalidates when?)

**Query performance:** <20ms (single MongoDB aggregation with proper indexes)

**Frontend action needed:** ✅ **NONE! This is the big win of v2.3.0**

---

## What This Means for Your Frontend

### ✅ You DON'T Need to Worry About:

1. **Team membership changes**
   - User added to team → Shows up immediately on next request
   - User removed from team → Access denied immediately
   - No cache invalidation needed!

2. **Role changes**
   - User promoted to admin → New role appears immediately
   - Permissions updated → New permissions appear immediately

3. **Organization switching**
   - Switch from Org A to Org B → Correct teams loaded automatically
   - No stale state possible!

### ⚠️ You MIGHT Need to Handle (Rare):

**Organization entitlement changes** (admin-only actions):
- If an admin upgrades the organization's subscription
- Impact: New features might take up to 1 hour to appear
- Solution: Either wait 1 hour OR have backend admin endpoint clear cache

---

## Caching Flow Diagram

### First Request (Cache Miss)

```
Frontend Request
  ↓
1. Extract & verify token → Check Redis cache
   Cache MISS
  ↓
2. Verify with Stytch API (50-80ms)
  ↓
3. Load org entitlements → Check Redis cache
   Cache MISS
  ↓
4. Query MongoDB for entitlements (10-20ms)
   Cache for 1 hour
  ↓
5. Load user context → Check Redis for user_id
   Cache MISS
  ↓
6. Query MongoDB for available_teams (10-20ms)
   DON'T cache teams (always fresh)
   Cache only user_id for 1 hour
  ↓
7. Build StytchContext
  ↓
8. Cache token verification result
  ↓
Response with fresh available_teams[]

Total: ~80-130ms (first request)
```

### Subsequent Requests (Cache Hit)

```
Frontend Request
  ↓
1. Extract & verify token → Check Redis cache
   Cache HIT ✅
  ↓
2. Load org entitlements → Check Redis cache
   Cache HIT ✅
  ↓
3. Load user context → Check Redis for user_id
   Cache HIT ✅ (user_id)
  ↓
4. Query MongoDB for available_teams (ALWAYS FRESH)
   Query memberships (10-20ms)
  ↓
5. Build StytchContext
  ↓
Response with fresh available_teams[]

Total: ~10-20ms (cached, but teams still fresh!)
```

---

## Cache Key Reference

### Default Prefix
```bash
# Set via STYTCH_CACHE_PREFIX env var (default: "ayz_auth")
STYTCH_CACHE_PREFIX=ayz_auth
```

### All Cache Keys

```bash
# 1. Session token verification
ayz_auth:token:<sha256_hash>

# Example:
ayz_auth:token:a7f8d9e2c4b6f1a3...

# 2. Organization entitlements
ayz_auth:entitlements:org:<stytch_org_id>

# Example:
ayz_auth:entitlements:org:organization-live-abc123

# 3. User ID (org-scoped)
ayz_auth:user_context:<stytch_member_id>:org:<stytch_org_id>

# Example:
ayz_auth:user_context:member-live-xyz789:org:organization-live-abc123

# 4. User ID (legacy, no org scope)
ayz_auth:user_context:<stytch_member_id>

# Example:
ayz_auth:user_context:member-live-xyz789
```

---

## Cache Invalidation Scenarios

### Scenario 1: User Added to Team

**What happens:**
- Admin adds user to a new team in MongoDB

**Cache impact:**
- ✅ **No cache invalidation needed!**
- Next request automatically loads fresh teams
- New team appears immediately in `available_teams[]`

**Frontend action:** None - just make next request

---

### Scenario 2: User Removed from Team

**What happens:**
- Admin removes user from a team in MongoDB

**Cache impact:**
- ✅ **No cache invalidation needed!**
- Next request automatically loads fresh teams
- Team disappears from `available_teams[]`
- Access denied if user tries to access that team

**Frontend action:** None - API will return 403 automatically

---

### Scenario 3: User Role Changed

**What happens:**
- User promoted from "member" to "admin"

**Cache impact:**
- ✅ **No cache invalidation needed!**
- Next request loads fresh role from MongoDB
- New role appears in `available_teams[].role`

**Frontend action:** None - new permissions take effect immediately

---

### Scenario 4: Organization Subscription Changed (Rare)

**What happens:**
- Admin upgrades org from "free" to "premium"

**Cache impact:**
- ⚠️ Entitlements cache might be stale for up to 1 hour
- New entitlements appear after:
  - Immediate: If cache cleared
  - 1 hour: If cache not cleared (TTL expires)

**Frontend action (optional):**
```python
# Backend admin endpoint (if you want instant updates)
@router.post("/admin/clear-entitlements-cache")
async def clear_entitlements_cache(
    auth: StytchContext = Depends(verify_auth)
):
    # Verify admin access...

    # Clear cache
    import redis
    r = redis.Redis()
    cache_key = f"ayz_auth:entitlements:org:{auth.organization_id}"
    r.delete(cache_key)

    return {"message": "Cache cleared, reload to see new entitlements"}
```

**Frequency:** Very rare (subscription changes)

---

### Scenario 5: User Switches Organizations

**What happens:**
- User clicks to switch from Org A to Org B in frontend

**Cache impact:**
- ✅ **Works perfectly!**
- New token for Org B has different org_id
- Cache keys are org-scoped
- Loads correct teams for Org B automatically

**Frontend action:** None - just use the new org's token

**Example:**
```javascript
// User switches to Org B
const newToken = await stytch.switchOrganization(orgB_id);

// Next API request automatically gets Org B's teams
fetch('/api/sessions', {
    headers: {
        'Authorization': `Bearer ${newToken}`,
        'X-Current-Team-Id': orgB_team_id
    }
});
// available_teams[] will have Org B teams only!
```

---

## Performance Characteristics

### v2.2.1 (Old)
```
First request:  80-130ms  (3-4 queries + validation)
Cached request: 20-50ms   (still validates team via MongoDB)
Team change:    Requires cache invalidation (complex)
```

### v2.3.0 (New)
```
First request:  50-80ms   (1 aggregation query)
Cached request: 10-20ms   (teams loaded fresh, but fast)
Team change:    No cache invalidation needed (always fresh)
```

**Net improvement:**
- 30-50ms faster on first request
- 2x faster on cached requests
- Simpler cache management (no invalidation needed!)

---

## Common Questions

### Q: Why aren't teams cached?

**A:** Teams change frequently and cache invalidation would be complex:
- User joins/leaves teams
- Roles change
- Permissions update
- Team names change

By loading fresh from MongoDB (~10-20ms), we ensure:
- ✅ Always accurate data
- ✅ No stale state bugs
- ✅ No cache invalidation logic needed
- ✅ Simpler frontend code

The query is fast enough (<20ms) that caching isn't worth the complexity.

---

### Q: Does this impact performance?

**A:** No! It's actually faster overall:
- **v2.2.1:** Cached team still required validation (20-50ms query)
- **v2.3.0:** Fresh team from single aggregation (10-20ms)

Plus you eliminate 3-4 validation queries on first request.

---

### Q: What if I want to cache teams anyway?

**A:** Not recommended, but if you really need it:

**Option 1: Frontend caching (simple)**
```javascript
// Cache teams list in frontend for 5 minutes
const cachedTeams = localStorage.getItem('teams');
const cacheTime = localStorage.getItem('teams_cached_at');

if (cachedTeams && Date.now() - cacheTime < 300000) {
    // Use cached teams
    return JSON.parse(cachedTeams);
}

// Fetch fresh from API
const response = await fetch('/api/auth/me');
const auth = await response.json();
localStorage.setItem('teams', JSON.stringify(auth.available_teams));
localStorage.setItem('teams_cached_at', Date.now());
```

**Option 2: Backend caching (complex, not recommended)**
- Would require invalidation on every team membership change
- Would need cache invalidation endpoints
- Would add complexity without much benefit

---

### Q: How do I debug cache issues?

**A:** Check Redis directly:

```bash
# Connect to Redis
redis-cli

# List all ayz_auth keys
KEYS ayz_auth:*

# Check specific key
GET ayz_auth:entitlements:org:organization-live-abc123

# Check TTL
TTL ayz_auth:entitlements:org:organization-live-abc123
# Returns seconds until expiration

# Delete specific key (force fresh load)
DEL ayz_auth:entitlements:org:organization-live-abc123
```

---

### Q: What happens if Redis is down?

**A:** Graceful degradation:
1. Cache read fails → Log warning
2. Load fresh from Stytch API / MongoDB
3. Cache write fails → Log warning
4. Continue without caching
5. Performance impact: Slower (no caching benefit)
6. Functionality: ✅ Still works!

---

## Configuration

### Cache Settings

Set these via environment variables:

```bash
# Cache TTL for session tokens (default: 300 seconds = 5 minutes)
STYTCH_CACHE_TTL=300

# Cache key prefix (default: ayz_auth)
STYTCH_CACHE_PREFIX=ayz_auth

# Redis connection
STYTCH_REDIS_URL=redis://localhost:6379
STYTCH_REDIS_PASSWORD=your_password  # Optional
STYTCH_REDIS_DB=0                    # Optional
```

### Recommended Settings

**Production:**
```bash
STYTCH_CACHE_TTL=300        # 5 minutes (default)
STYTCH_CACHE_PREFIX=ayz_auth
```

**Development:**
```bash
STYTCH_CACHE_TTL=60         # 1 minute (faster testing)
STYTCH_CACHE_PREFIX=ayz_auth_dev
```

---

## Frontend Best Practices

### 1. Trust the Team List ✅

```javascript
// The available_teams list is ALWAYS current
const auth = await getAuthContext();

// No need to invalidate or refresh - it's always fresh!
const teams = auth.available_teams;

// Safe to use immediately
const currentTeam = teams.find(t => t.id === selectedTeamId);
```

### 2. Handle Team Access Denied

```javascript
// If user tries to access a team they were just removed from
try {
    const response = await fetch('/api/sessions', {
        headers: {
            'Authorization': `Bearer ${token}`,
            'X-Current-Team-Id': teamId
        }
    });
} catch (error) {
    if (error.status === 403) {
        // User no longer has access to this team
        // Fetch fresh auth context and show available teams
        const auth = await getAuthContext();
        showTeamSelector(auth.available_teams);
    }
}
```

### 3. No Need for Cache Invalidation Endpoints

```javascript
// ❌ You DON'T need this anymore!
async function invalidateTeamCache() {
    await fetch('/api/cache/invalidate/teams', { method: 'POST' });
}

// ✅ Just make the next request - teams are always fresh!
const auth = await getAuthContext();
// auth.available_teams is current!
```

---

## Backend Cache Management

### When to Clear Cache (Admin Actions Only)

#### Clear Organization Entitlements Cache

**Trigger:** Organization subscription changed

```python
from ayz_auth.cache.redis_client import redis_client

@router.post("/admin/organizations/{org_id}/subscription")
async def update_subscription(
    org_id: str,
    subscription_data: SubscriptionUpdate,
    auth: StytchContext = Depends(verify_auth)
):
    # Verify admin access...

    # Update subscription in MongoDB
    await db.organizations.update_one(
        {"stytch_org_id": org_id},
        {"$set": {
            "subscription_tier": subscription_data.tier,
            "entitlements": subscription_data.entitlements
        }}
    )

    # Clear cache so changes take effect immediately
    await redis_client.delete_cached_organization_entitlements(org_id)

    return {"message": "Subscription updated"}
```

#### NO Need to Clear Team Cache

**Why:** There is no team cache! Teams are always fresh.

```python
# ❌ You DON'T need this:
async def clear_team_cache(user_id: str, org_id: str):
    # No team cache exists!
    pass

# ✅ Just update MongoDB - next request gets fresh data:
@router.post("/admin/teams/{team_id}/members/{user_id}")
async def add_team_member(team_id: str, user_id: str):
    # Add to team in MongoDB
    await db.user_team_memberships.insert_one({
        "user_id": ObjectId(user_id),
        "team_id": ObjectId(team_id),
        "status": "active",
        "role_id": ObjectId(role_id)
    })

    # No cache to clear!
    return {"message": "User added to team"}
```

---

## Cache Invalidation API (Optional Backend Endpoint)

If you want to provide cache clearing for admins:

```python
from fastapi import APIRouter
from ayz_auth import StytchContext, verify_auth
from ayz_auth.cache.redis_client import redis_client

admin_router = APIRouter(prefix="/admin/cache")

@admin_router.delete("/entitlements/{org_id}")
async def clear_org_entitlements_cache(
    org_id: str,
    auth: StytchContext = Depends(verify_auth)
):
    """Clear cached entitlements for an organization (admin only)"""

    # Verify admin role...
    if "admin" not in auth.entitlements:
        raise HTTPException(403, "Admin access required")

    # Clear cache
    await redis_client.delete_cached_organization_entitlements(org_id)

    return {
        "message": f"Entitlements cache cleared for org {org_id}",
        "note": "Team context is never cached - always fresh"
    }

@admin_router.delete("/all")
async def clear_all_cache(
    auth: StytchContext = Depends(verify_auth)
):
    """Clear all caches (super admin only)"""

    # Verify super admin...

    import redis
    r = redis.Redis()
    keys = r.keys("ayz_auth:*")
    if keys:
        r.delete(*keys)
        return {"message": f"Cleared {len(keys)} cache keys"}

    return {"message": "No cache keys found"}
```

---

## Monitoring Cache Performance

### Useful Redis Commands

```bash
# Check cache hit rate
redis-cli INFO stats | grep hit

# Count ayz_auth keys
redis-cli KEYS "ayz_auth:*" | wc -l

# Check memory usage
redis-cli INFO memory | grep used_memory_human

# Monitor cache operations in real-time
redis-cli MONITOR | grep ayz_auth
```

### Logging

ayz-auth logs cache operations:

```python
# Cache hits
logger.debug("Cache hit for token hash: a7f8d9e2...")
logger.debug("Cache hit for organization entitlements: org-123")

# Cache misses
logger.debug("Cache miss for token hash: a7f8d9e2...")
logger.debug("Cache miss for organization entitlements: org-123")

# Team loading (always fresh)
logger.debug("Found 5 teams for user user-456 in org org-789")
```

Set `STYTCH_LOG_LEVEL=DEBUG` to see cache operations.

---

## Summary

### What Your Frontend Needs to Know

1. **Team changes are instant** - no cache invalidation needed
2. **Organization switching works perfectly** - org-scoped caching
3. **Subscription changes** - might take up to 1 hour (rare event)
4. **No special cache handling code needed** - it just works!

### Key Differences from v2.2.x

| Aspect | v2.2.x (Old) | v2.3.0 (New) |
|--------|--------------|--------------|
| **Team caching** | current_team_id cached (5 min) | available_teams[] always fresh ✅ |
| **Cache invalidation** | Needed when teams change | Never needed ✅ |
| **Stale state bugs** | Common (1-2% error rate) | Eliminated ✅ |
| **Performance** | 80-130ms | 50-80ms ✅ |
| **Complexity** | High (validation + invalidation) | Low (just load fresh) ✅ |

---

## Bottom Line

**Your frontend doesn't need to worry about cache invalidation for team context in v2.3.0.**

The `available_teams[]` list is always fresh, so:
- ✅ No cache invalidation API calls needed
- ✅ No cache-busting logic needed
- ✅ No stale state bugs
- ✅ Simpler frontend code
- ✅ Better user experience

Just make your API requests and trust that the team list is current! 🚀
