Caching Architecture
This document describes the caching architecture implemented in the application to improve performance and reduce database load.
Overview
Section titled “Overview”The application uses a dual-layer caching strategy:
- Server-side caching (Edge Functions) - Redis (Upstash) with in-memory fallback
- Client-side caching (Frontend) - React Query with optimized configurations
This design ensures the app remains portable - it works perfectly without Redis for development, Docker deployments, or small-scale production use.
Server-Side Caching (Edge Functions)
Section titled “Server-Side Caching (Edge Functions)”Architecture
Section titled “Architecture”┌─────────────────────────────────────────────────────────┐│ Edge Function │├─────────────────────────────────────────────────────────┤│ Request → Cache Check → Cache Hit? → Response ││ │ │ ││ │ No ││ │ ↓ ││ │ Database Query ││ │ │ ││ │ Cache Store ││ │ ↓ ││ └──────────Response │└─────────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────┐ │ Cache Abstraction │ │ │ │ Redis Configured? │ │ Yes → Upstash Redis (HTTP) │ │ No → In-Memory Map │ └─────────────────────────────────────┘| File | Description |
|---|---|
supabase/functions/_shared/cache.ts | Core cache abstraction layer |
supabase/functions/_shared/cache-utils.ts | High-level caching utilities |
supabase/functions/_shared/rate-limiter.ts | Rate limiting with cache support |
Configuration
Section titled “Configuration”Redis is optional. Set these environment variables to enable:
UPSTASH_REDIS_REST_URL=https://us1-example-12345.upstash.ioUPSTASH_REDIS_REST_TOKEN=AXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxCache Key Patterns
Section titled “Cache Key Patterns”// Pre-defined key buildersCacheKeys.rateLimit(prefix, identifier) // ratelimit:{prefix}:{identifier}CacheKeys.qrmMetrics(cellId, tenantId) // qrm:cell:{cellId}:{tenantId}CacheKeys.dashboardStats(tenantId) // dashboard:stats:{tenantId}CacheKeys.operationDetails(operationId) // operation:{operationId}CacheKeys.tenantSettings(tenantId) // tenant:settings:{tenantId}TTL Presets
Section titled “TTL Presets”CacheTTL.RATE_LIMIT // 60 secondsCacheTTL.SHORT // 30 secondsCacheTTL.MEDIUM // 2 minutesCacheTTL.LONG // 5 minutesCacheTTL.VERY_LONG // 15 minutesCacheTTL.HOUR // 1 hourCacheTTL.DAY // 24 hoursUsage Examples
Section titled “Usage Examples”import { getCache, getCachedJson, setCachedJson, CacheKeys, CacheTTL } from '../_shared/cache.ts';import { cacheOrFetch, getCachedQRMMetrics } from '../_shared/cache-utils.ts';
// Simple get/setconst cache = getCache();await cache.set('my-key', 'my-value', 300); // 5 minute TTLconst value = await cache.get('my-key');
// JSON helpersawait setCachedJson('user:123', { name: 'John' }, 600);const user = await getCachedJson<User>('user:123');
// Cache-or-fetch patternconst data = await cacheOrFetch( CacheKeys.dashboardStats(tenantId), CacheTTL.LONG, async () => { // Expensive database query return await fetchDashboardStats(tenantId); });
// Pre-built QRM metrics cachingconst metrics = await getCachedQRMMetrics(supabase, cellId, tenantId);Rate Limiting with Cache
Section titled “Rate Limiting with Cache”import { checkRateLimit, createRateLimitResponse } from '../_shared/rate-limiter.ts';
// Async version (uses Redis if available)const result = await checkRateLimit(apiKey, { maxRequests: 100, windowMs: 60000, // 1 minute keyPrefix: 'api',});
if (!result.allowed) { return createRateLimitResponse(result, corsHeaders);}
// Sync version (in-memory only, for backward compatibility)const syncResult = checkRateLimitSync(apiKey, config);Client-Side Caching (React Query)
Section titled “Client-Side Caching (React Query)”Architecture
Section titled “Architecture”React Query provides automatic caching with configurable stale times and garbage collection.
| File | Description |
|---|---|
src/lib/queryClient.ts | QueryClient configuration and presets |
src/lib/cacheInvalidation.ts | Cache invalidation utilities |
Query Keys Factory
Section titled “Query Keys Factory”Use consistent query keys for proper cache invalidation:
import { QueryKeys } from '@/lib/queryClient';
// JobsQueryKeys.jobs.all(tenantId)QueryKeys.jobs.detail(jobId)
// OperationsQueryKeys.operations.all(tenantId)QueryKeys.operations.byCell(cellId)QueryKeys.operations.workQueue(cellId)
// Cells & QRMQueryKeys.cells.all(tenantId)QueryKeys.cells.qrmMetrics(cellId, tenantId)
// DashboardQueryKeys.dashboard.stats(tenantId)Stale Time Presets
Section titled “Stale Time Presets”import { StaleTime, CacheTime, defaultQueryOptions } from '@/lib/queryClient';
// Stale time presetsStaleTime.NONE // 0 - Always refetchStaleTime.VERY_SHORT // 10 secondsStaleTime.SHORT // 30 seconds (default)StaleTime.MEDIUM // 2 minutesStaleTime.LONG // 5 minutesStaleTime.VERY_LONG // 15 minutes
// Pre-configured options for common scenariosdefaultQueryOptions.realtime // For work queues, active operationsdefaultQueryOptions.lists // For job lists, part listsdefaultQueryOptions.details // For detail viewsdefaultQueryOptions.config // For configuration datadefaultQueryOptions.static // For rarely changing dataUsage Examples
Section titled “Usage Examples”import { useQuery, useQueryClient } from '@tanstack/react-query';import { QueryKeys, StaleTime, defaultQueryOptions } from '@/lib/queryClient';import { invalidateOperationCaches } from '@/lib/cacheInvalidation';
// Using query keys and stale timeconst { data: jobs } = useQuery({ queryKey: QueryKeys.jobs.all(tenantId), queryFn: fetchJobs, staleTime: StaleTime.SHORT,});
// Using preset optionsconst { data: cells } = useQuery({ queryKey: QueryKeys.cells.all(tenantId), queryFn: fetchCells, ...defaultQueryOptions.config,});
// Cache invalidation after mutationsconst queryClient = useQueryClient();
const mutation = useMutation({ mutationFn: updateOperation, onSuccess: (_, variables) => { invalidateOperationCaches( queryClient, tenantId, variables.operationId, variables.cellId ); },});Docker / Local Development
Section titled “Docker / Local Development”The caching system works seamlessly without Redis:
services: app: build: . environment: - VITE_SUPABASE_URL=... - VITE_SUPABASE_PUBLISHABLE_KEY=... # Redis variables not set = uses in-memory cacheFor local development with Redis (optional):
services: app: build: . environment: - UPSTASH_REDIS_REST_URL=http://redis:8079 - UPSTASH_REDIS_REST_TOKEN=local-token depends_on: - redis
redis: image: redis:alpine ports: - "6379:6379"Best Practices
Section titled “Best Practices”1. Cache Invalidation
Section titled “1. Cache Invalidation”Always invalidate related caches when data changes:
// After updating an operationinvalidateOperationCaches(queryClient, tenantId, operationId, cellId);
// After updating a jobinvalidateJobCaches(queryClient, tenantId, jobId);2. Choosing TTL Values
Section titled “2. Choosing TTL Values”| Data Type | Recommended TTL | Reason |
|---|---|---|
| Active operations | 10-30 seconds | Real-time display |
| Job lists | 30-60 seconds | Moderate update frequency |
| Configuration | 5-15 minutes | Rarely changes |
| User profiles | 5 minutes | Session-stable |
| Tenant settings | 15-30 minutes | Admin changes only |
3. Cache Keys
Section titled “3. Cache Keys”- Always include
tenantIdfor multi-tenant data - Use the
CacheKeyshelper for consistency - Keep keys descriptive but concise
4. Error Handling
Section titled “4. Error Handling”The cache layer gracefully handles errors:
// Redis errors fall back to memoryconst cache = getCache();await cache.set('key', 'value'); // Uses Redis or memory transparently
// Check cache type for debuggingconsole.log(`Using ${cache.getType()} cache`);Monitoring
Section titled “Monitoring”Cache Health Check
Section titled “Cache Health Check”import { getCacheHealth } from '../_shared/cache-utils.ts';
const health = await getCacheHealth();console.log(`Cache: ${health.type}, Healthy: ${health.healthy}, Latency: ${health.latencyMs}ms`);Recommended Metrics
Section titled “Recommended Metrics”When using Redis, monitor:
- Cache hit rate
- Memory usage
- Connection errors
- Latency percentiles
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”1. Rate limiting not persisting across restarts
- Check if Redis credentials are set correctly
- Verify Upstash dashboard shows connections
2. Stale data in UI
- Ensure cache invalidation is called after mutations
- Check staleTime configuration
- Verify real-time subscriptions are working
3. Memory issues in Edge Functions
- In-memory cache grows unbounded in long-running functions
- Consider reducing TTL or adding size limits
- Switch to Redis for production workloads
Debug Logging
Section titled “Debug Logging”Enable cache debug logging:
// In Edge Functionconst cache = getCache();console.log(`[Cache] Type: ${cache.getType()}`);console.log(`[Cache] Redis configured: ${isRedisConfigured()}`);