Redis Integration Guide for HG Content System¶
Overview¶
Redis integration using Upstash for enhanced performance, caching, and analytics across the HG Content platform deployed on Vercel.
Architecture¶
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Vercel Apps │ │ Upstash Redis │ │ Analytics │
│ │ │ │ │ │
│ • Marketing │◄──►│ • Rate Limiting │◄──►│ • Performance │
│ • Dashboard │ │ • Session Store │ │ • Usage Stats │
│ • Documentation │ │ • Cache Layer │ │ • User Behavior │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Upstash Redis Setup¶
1. Account Creation¶
- Visit https://upstash.com/
- Create account (free tier available)
- Create new Redis database
- Select region closest to Vercel deployment
- Copy connection details
2. Database Configuration¶
Region: us-east-1 (or closest to your users)
Type: Regional (for lower latency)
TLS: Enabled (for security)
Eviction: allkeys-lru (for caching)
3. Connection Details¶
# From Upstash Dashboard
REDIS_URL=redis://default:your-password@your-endpoint:port
UPSTASH_REDIS_REST_URL=https://your-endpoint.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token
Environment Configuration¶
Vercel Environment Variables¶
# Redis Configuration (Production)
REDIS_URL=redis://default:password@endpoint:port
UPSTASH_REDIS_REST_URL=https://endpoint.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token
# Redis Configuration (Preview/Development)
REDIS_URL_PREVIEW=redis://default:dev-password@dev-endpoint:port
UPSTASH_REDIS_REST_URL_PREVIEW=https://dev-endpoint.upstash.io
UPSTASH_REDIS_REST_TOKEN_PREVIEW=dev-token
Local Development¶
Implementation¶
1. Redis Client Setup¶
// lib/redis.ts
import { Redis } from '@upstash/redis'
// Initialize Redis client
export const redis = Redis.fromEnv()
// Alternative initialization with explicit config
export const redisClient = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})
// Helper functions
export async function setCache(key: string, value: any, ttl = 3600) {
return await redis.setex(key, ttl, JSON.stringify(value))
}
export async function getCache(key: string) {
const value = await redis.get(key)
return value ? JSON.parse(value as string) : null
}
export async function deleteCache(key: string) {
return await redis.del(key)
}
2. Rate Limiting Implementation¶
// lib/rate-limit.ts
import { redis } from './redis'
export async function rateLimit(
identifier: string,
limit = 100,
window = 3600 // 1 hour in seconds
) {
const key = `rate_limit:${identifier}`
const current = await redis.incr(key)
if (current === 1) {
// Set expiry on first request
await redis.expire(key, window)
}
return {
success: current <= limit,
remaining: Math.max(0, limit - current),
resetTime: Date.now() + (window * 1000)
}
}
// API route usage
// pages/api/contact.ts
import { rateLimit } from '@/lib/rate-limit'
export default async function handler(req, res) {
const ip = req.headers['x-forwarded-for'] || 'unknown'
const { success, remaining, resetTime } = await rateLimit(ip, 10, 3600)
if (!success) {
return res.status(429).json({
error: 'Rate limit exceeded',
resetTime
})
}
res.setHeader('X-RateLimit-Remaining', remaining)
// Process request...
}
3. Session Storage¶
// lib/session.ts
import { redis } from './redis'
import { nanoid } from 'nanoid'
export interface SessionData {
userId?: string
email?: string
preferences?: any
lastActivity: number
}
export async function createSession(data: Partial<SessionData>): Promise<string> {
const sessionId = nanoid()
const sessionData: SessionData = {
...data,
lastActivity: Date.now()
}
await redis.setex(
`session:${sessionId}`,
86400, // 24 hours
JSON.stringify(sessionData)
)
return sessionId
}
export async function getSession(sessionId: string): Promise<SessionData | null> {
const data = await redis.get(`session:${sessionId}`)
if (!data) return null
const session: SessionData = JSON.parse(data as string)
// Update last activity
session.lastActivity = Date.now()
await redis.setex(`session:${sessionId}`, 86400, JSON.stringify(session))
return session
}
export async function destroySession(sessionId: string): Promise<void> {
await redis.del(`session:${sessionId}`)
}
4. Analytics Caching¶
// lib/analytics-cache.ts
import { redis } from './redis'
export async function cachePageView(page: string, userAgent?: string) {
const today = new Date().toISOString().split('T')[0]
const hourKey = new Date().toISOString().substring(0, 13)
// Daily page views
await redis.incr(`analytics:daily:${today}:${page}`)
await redis.expire(`analytics:daily:${today}:${page}`, 86400 * 30) // 30 days
// Hourly page views
await redis.incr(`analytics:hourly:${hourKey}:${page}`)
await redis.expire(`analytics:hourly:${hourKey}:${page}`, 86400 * 7) // 7 days
// User agents (for device/browser analytics)
if (userAgent) {
await redis.sadd(`analytics:user_agents:${today}`, userAgent)
await redis.expire(`analytics:user_agents:${today}`, 86400 * 30)
}
}
export async function getPageViews(page: string, days = 7) {
const views = []
for (let i = 0; i < days; i++) {
const date = new Date()
date.setDate(date.getDate() - i)
const dateKey = date.toISOString().split('T')[0]
const count = await redis.get(`analytics:daily:${dateKey}:${page}`)
views.push({
date: dateKey,
views: count || 0
})
}
return views.reverse()
}
export async function getTopPages(days = 7) {
const pipeline = redis.pipeline()
const dates = []
for (let i = 0; i < days; i++) {
const date = new Date()
date.setDate(date.getDate() - i)
dates.push(date.toISOString().split('T')[0])
}
// Get all page view keys for the date range
const keys = await redis.keys(`analytics:daily:${dates[0]}:*`)
const pages = new Map()
for (const key of keys) {
const page = key.split(':').slice(3).join(':')
const count = await redis.get(key)
pages.set(page, (pages.get(page) || 0) + parseInt(count as string || '0'))
}
return Array.from(pages.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
}
5. Documentation Feedback System¶
// lib/documentation-feedback.ts
import { redis } from './redis'
export interface DocumentationFeedback {
pageId: string
rating: number // 1-5
comment?: string
email?: string
timestamp: number
userAgent: string
ip: string
}
export async function submitFeedback(feedback: DocumentationFeedback) {
const feedbackId = `${feedback.pageId}:${Date.now()}`
// Store individual feedback
await redis.setex(
`feedback:item:${feedbackId}`,
86400 * 90, // 90 days
JSON.stringify(feedback)
)
// Add to page feedback list
await redis.lpush(`feedback:page:${feedback.pageId}`, feedbackId)
await redis.expire(`feedback:page:${feedback.pageId}`, 86400 * 90)
// Update average rating
await updatePageRating(feedback.pageId, feedback.rating)
return feedbackId
}
async function updatePageRating(pageId: string, newRating: number) {
const ratingsKey = `ratings:${pageId}`
// Get current rating data
const currentData = await redis.hmget(ratingsKey, 'total', 'count')
const total = parseInt(currentData[0] as string || '0')
const count = parseInt(currentData[1] as string || '0')
// Update with new rating
const newTotal = total + newRating
const newCount = count + 1
await redis.hmset(ratingsKey, {
total: newTotal,
count: newCount,
average: (newTotal / newCount).toFixed(2)
})
await redis.expire(ratingsKey, 86400 * 90)
}
export async function getPageFeedback(pageId: string, limit = 10) {
const feedbackIds = await redis.lrange(`feedback:page:${pageId}`, 0, limit - 1)
const feedback = []
for (const id of feedbackIds) {
const data = await redis.get(`feedback:item:${id}`)
if (data) {
feedback.push(JSON.parse(data as string))
}
}
return feedback
}
export async function getPageRating(pageId: string) {
const rating = await redis.hmget(`ratings:${pageId}`, 'average', 'count')
return {
average: parseFloat(rating[0] as string || '0'),
count: parseInt(rating[1] as string || '0')
}
}
6. Content Caching¶
// lib/content-cache.ts
import { redis } from './redis'
export async function cacheApiResponse(endpoint: string, params: any, response: any, ttl = 300) {
const cacheKey = `api:${endpoint}:${JSON.stringify(params)}`
await redis.setex(cacheKey, ttl, JSON.stringify(response))
}
export async function getCachedApiResponse(endpoint: string, params: any) {
const cacheKey = `api:${endpoint}:${JSON.stringify(params)}`
const cached = await redis.get(cacheKey)
return cached ? JSON.parse(cached as string) : null
}
// Usage in API routes
// pages/api/content/generate.ts
import { getCachedApiResponse, cacheApiResponse } from '@/lib/content-cache'
export default async function handler(req, res) {
const cacheKey = `content:${JSON.stringify(req.body)}`
// Check cache first
const cached = await getCachedApiResponse('generate', req.body)
if (cached) {
return res.json(cached)
}
// Generate new content
const response = await generateContent(req.body)
// Cache the response (5 minutes)
await cacheApiResponse('generate', req.body, response, 300)
res.json(response)
}
API Integration Examples¶
Next.js API Route with Redis¶
// pages/api/analytics/page-views.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { cachePageView, getPageViews } from '@/lib/analytics-cache'
import { rateLimit } from '@/lib/rate-limit'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const ip = req.headers['x-forwarded-for'] as string || 'unknown'
// Rate limiting
const { success } = await rateLimit(ip, 1000, 3600) // 1000 requests per hour
if (!success) {
return res.status(429).json({ error: 'Rate limit exceeded' })
}
if (req.method === 'POST') {
const { page, userAgent } = req.body
await cachePageView(page, userAgent)
res.json({ success: true })
} else if (req.method === 'GET') {
const { page, days = 7 } = req.query
const views = await getPageViews(page as string, parseInt(days as string))
res.json({ views })
} else {
res.status(405).json({ error: 'Method not allowed' })
}
}
Documentation Feedback API¶
// pages/api/docs/feedback.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { submitFeedback, getPageFeedback, getPageRating } from '@/lib/documentation-feedback'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
const feedback = {
...req.body,
timestamp: Date.now(),
userAgent: req.headers['user-agent'] || '',
ip: req.headers['x-forwarded-for'] as string || 'unknown'
}
const id = await submitFeedback(feedback)
res.json({ success: true, id })
} else if (req.method === 'GET') {
const { pageId } = req.query
const [feedback, rating] = await Promise.all([
getPageFeedback(pageId as string),
getPageRating(pageId as string)
])
res.json({ feedback, rating })
} else {
res.status(405).json({ error: 'Method not allowed' })
}
}
Frontend Integration¶
React Hook for Analytics¶
// hooks/useAnalytics.ts
import { useEffect } from 'react'
export function usePageView() {
useEffect(() => {
// Track page view
fetch('/api/analytics/page-views', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
page: window.location.pathname,
userAgent: navigator.userAgent
})
}).catch(console.error)
}, [])
}
// Usage in pages
export default function HomePage() {
usePageView()
return (
<div>
{/* Page content */}
</div>
)
}
Documentation Feedback Component¶
// components/DocumentationFeedback.tsx
import { useState } from 'react'
export function DocumentationFeedback({ pageId }: { pageId: string }) {
const [rating, setRating] = useState(0)
const [comment, setComment] = useState('')
const [submitted, setSubmitted] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
await fetch('/api/docs/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pageId, rating, comment })
})
setSubmitted(true)
}
if (submitted) {
return <div className="text-green-600">Thank you for your feedback!</div>
}
return (
<form onSubmit={handleSubmit} className="mt-8 p-4 border rounded">
<h3 className="font-semibold mb-4">Was this page helpful?</h3>
<div className="flex gap-2 mb-4">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
onClick={() => setRating(star)}
className={`text-2xl ${star <= rating ? 'text-yellow-400' : 'text-gray-300'}`}
>
⭐
</button>
))}
</div>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Optional: Tell us how we can improve this page"
className="w-full p-2 border rounded mb-4"
rows={3}
/>
<button
type="submit"
disabled={rating === 0}
className="bg-blue-500 text-white px-4 py-2 rounded disabled:bg-gray-300"
>
Submit Feedback
</button>
</form>
)
}
Performance Monitoring¶
Redis Performance Metrics¶
// lib/redis-metrics.ts
import { redis } from './redis'
export async function getRedisMetrics() {
const info = await redis.info()
const memory = await redis.info('memory')
const stats = await redis.info('stats')
return {
connectedClients: parseInt(info.split('\r\n').find(line => line.startsWith('connected_clients:'))?.split(':')[1] || '0'),
usedMemory: parseInt(memory.split('\r\n').find(line => line.startsWith('used_memory:'))?.split(':')[1] || '0'),
totalCommandsProcessed: parseInt(stats.split('\r\n').find(line => line.startsWith('total_commands_processed:'))?.split(':')[1] || '0'),
keyspaceHits: parseInt(stats.split('\r\n').find(line => line.startsWith('keyspace_hits:'))?.split(':')[1] || '0'),
keyspaceMisses: parseInt(stats.split('\r\n').find(line => line.startsWith('keyspace_misses:'))?.split(':')[1] || '0')
}
}
// Cache hit rate calculation
export async function getCacheHitRate() {
const metrics = await getRedisMetrics()
const total = metrics.keyspaceHits + metrics.keyspaceMisses
return total > 0 ? (metrics.keyspaceHits / total) * 100 : 0
}
Performance Dashboard¶
// pages/api/admin/redis-stats.ts
import { getRedisMetrics, getCacheHitRate } from '@/lib/redis-metrics'
export default async function handler(req, res) {
// Add authentication check here
const metrics = await getRedisMetrics()
const hitRate = await getCacheHitRate()
res.json({
...metrics,
cacheHitRate: hitRate,
timestamp: Date.now()
})
}
Best Practices¶
1. Key Naming Convention¶
Pattern: service:feature:identifier:details
Examples:
- analytics:daily:2024-01-15:home
- session:user:abc123
- cache:api:generate:hash123
- feedback:page:docs/api-reference
- rate_limit:ip:192.168.1.1
2. TTL Strategy¶
const TTL = {
RATE_LIMIT: 3600, // 1 hour
SESSION: 86400, // 24 hours
API_CACHE: 300, // 5 minutes
ANALYTICS: 2592000, // 30 days
FEEDBACK: 7776000, // 90 days
} as const
3. Error Handling¶
// lib/redis-safe.ts
import { redis } from './redis'
export async function safeRedisOperation<T>(
operation: () => Promise<T>,
fallback: T,
logError = true
): Promise<T> {
try {
return await operation()
} catch (error) {
if (logError) {
console.error('Redis operation failed:', error)
}
return fallback
}
}
// Usage
const cachedData = await safeRedisOperation(
() => redis.get('some-key'),
null
)
4. Connection Management¶
// lib/redis-connection.ts
let redisClient: Redis | null = null
export function getRedisClient() {
if (!redisClient) {
redisClient = Redis.fromEnv()
}
return redisClient
}
// Graceful shutdown
process.on('SIGTERM', async () => {
if (redisClient) {
console.log('Closing Redis connection')
// Upstash Redis connections are closed automatically
}
})
Monitoring and Alerts¶
Upstash Dashboard Monitoring¶
- Monitor memory usage
- Track command latency
- Set up alerts for high error rates
- Monitor connection counts
Custom Metrics¶
// pages/api/health/redis.ts
export default async function handler(req, res) {
try {
const start = Date.now()
await redis.ping()
const latency = Date.now() - start
res.json({
status: 'healthy',
latency: `${latency}ms`,
timestamp: new Date().toISOString()
})
} catch (error) {
res.status(500).json({
status: 'unhealthy',
error: error.message,
timestamp: new Date().toISOString()
})
}
}
Cost Optimization¶
Upstash Pricing Tiers¶
- Free Tier: 10,000 commands/day
- Pay-as-you-scale: $0.2 per 100K commands
- Fixed Plans: Starting at $10/month
Optimization Strategies¶
- Efficient Key Patterns: Use consistent, predictable key naming
- Appropriate TTLs: Don't store data longer than needed
- Batch Operations: Use pipelines for multiple operations
- Memory Management: Monitor memory usage and optimize data structures
- Connection Pooling: Reuse connections (handled automatically by Upstash)
Cost Monitoring¶
// Track Redis usage
export async function trackRedisUsage(operation: string) {
await redis.incr(`usage:${operation}:${new Date().toISOString().split('T')[0]}`)
}
// Get daily usage stats
export async function getDailyUsage(date: string) {
const keys = await redis.keys(`usage:*:${date}`)
const usage = {}
for (const key of keys) {
const operation = key.split(':')[1]
const count = await redis.get(key)
usage[operation] = parseInt(count as string || '0')
}
return usage
}