Skip to content

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

  1. Visit https://upstash.com/
  2. Create account (free tier available)
  3. Create new Redis database
  4. Select region closest to Vercel deployment
  5. 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

# .env.local
REDIS_URL=redis://localhost:6379
# Or use Upstash development instance

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

  1. Efficient Key Patterns: Use consistent, predictable key naming
  2. Appropriate TTLs: Don't store data longer than needed
  3. Batch Operations: Use pipelines for multiple operations
  4. Memory Management: Monitor memory usage and optimize data structures
  5. 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
}