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();
}