Skip to content

Redis

Use Redis for session storage, caching, and rate limiting in your Juice app. The setup varies by runtime, but the usage patterns are the same — share the client via context and use it in middleware, server actions, and routes.

Setup

Bun has a built-in Redis client. No dependencies required.

// server.ts
import { createContextKey, setContext } from '@cmj/juice/runtime';

const redis = new Bun.RedisClient(process.env.REDIS_URL);

export const redisKey = createContextKey<Bun.RedisClient>('redis');

// In your router config:
onBeforeRequest: async (req) => {
  setContext(req, redisKey, redis);
}

Workers cannot make TCP connections, so traditional Redis is not available. Use Workers KV for key-value patterns instead.

// Workers KV as a Redis alternative
// wrangler.toml:
// [[kv_namespaces]]
// binding = "MY_KV"
// id = "abc123"

// Usage via env binding:
await env.MY_KV.get(key);
await env.MY_KV.put(key, value, { expirationTtl: 3600 });
await env.MY_KV.delete(key);

For full Redis on Workers, consider Upstash Redis which provides an HTTP-based Redis client compatible with the Workers runtime.

Use ioredis for Node.js environments.

import Redis from 'ioredis';
import { createContextKey, setContext } from '@cmj/juice/runtime';

const redis = new Redis(process.env.REDIS_URL);

export const redisKey = createContextKey<Redis>('redis');

// In your router config:
onBeforeRequest: async (req) => {
  setContext(req, redisKey, redis);
}

Session Storage

Store sessions in Redis with a TTL. A middleware reads the session cookie, loads the session data from Redis, and shares it via context.

// middleware.ts
import { getContext, setContext, createContextKey } from '@cmj/juice/runtime';
import { redisKey } from '../../server.js';

interface Session {
  userId: string;
  email: string;
}

export const sessionKey = createContextKey<Session | null>('session');

export default async function sessionMiddleware(
  req: Request,
  next: () => Promise<Response>,
) {
  const redis = getContext(req, redisKey)!;
  const cookies = req.headers.get('cookie') ?? '';
  const match = cookies.match(/session_id=([^;]+)/);

  let session: Session | null = null;
  if (match) {
    const data = await redis.get(`session:${match[1]}`);
    if (data) session = JSON.parse(data);
  }

  setContext(req, sessionKey, session);
  return next();
}

On login, create the session in Redis and set the cookie:

import { randomUUID } from 'crypto';

async function login(formData: FormData) {
  'use server';
  const redis = getContext(this.request, redisKey)!;
  const userId = '...'; // after verifying credentials

  const sessionId = randomUUID();
  await redis.set(
    `session:${sessionId}`,
    JSON.stringify({ userId, email: formData.get('email') }),
    'EX',
    86400, // 24 hours
  );

  redirect('/', 303, {
    headers: {
      'Set-Cookie': `session_id=${sessionId}; HttpOnly; Path=/; SameSite=Lax; Max-Age=86400`,
    },
  });
}

Cache Layer

Use Redis as a cache adapter for Juice's cache() system. This lets cached server data persist across requests and server restarts.

// server.ts
createRouter(manifest, {
  cache: {
    get: (key) => redis.get(key).then(v => v ? JSON.parse(v) : undefined),
    set: (key, value, ttl) =>
      redis.set(key, JSON.stringify(value), 'EX', ttl ?? 3600),
  },
});

Rate Limiting

A simple sliding-window rate limiter using Redis INCR and EXPIRE. Place this middleware on routes that need protection.

// middleware/rate-limit.ts
import { getContext } from '@cmj/juice/runtime';
import { redisKey } from '../../server.js';

export default async function rateLimit(
  req: Request,
  next: () => Promise<Response>,
) {
  const redis = getContext(req, redisKey)!;
  const ip = req.headers.get('x-forwarded-for')
    ?? req.headers.get('cf-connecting-ip')
    ?? 'unknown';
  const key = `rl:${ip}`;

  const count = await redis.incr(key);
  if (count === 1) await redis.expire(key, 60);

  if (count > 100) {
    return new Response('Too Many Requests', {
      status: 429,
      headers: { 'Retry-After': '60' },
    });
  }

  return next();
}