@stateloom/persist-redis
Redis-backed storage adapter for @stateloom/persist. Bring your own Redis client -- works with ioredis, redis (node-redis), and @upstash/redis.
Install
pnpm add @stateloom/persist @stateloom/persist-redisnpm install @stateloom/persist @stateloom/persist-redisyarn add @stateloom/persist @stateloom/persist-redisSize: ~0.2 KB gzipped
Overview
The package provides a single factory function redisStorage() that creates a StorageAdapter backed by Redis. It delegates to a user-provided Redis client (BYO pattern), so there are zero runtime Redis dependencies. The adapter plugs directly into @stateloom/persist's storage option.
Quick Start
import Redis from 'ioredis';
import { createStore } from '@stateloom/store';
import { persist } from '@stateloom/persist';
import { redisStorage } from '@stateloom/persist-redis';
const store = createStore(
(set) => ({
theme: 'light' as 'light' | 'dark',
setTheme: (t: 'light' | 'dark') => set({ theme: t }),
}),
{
middleware: [
persist({
key: 'user-prefs',
storage: redisStorage({ client: new Redis() }),
}),
],
},
);Guide
Basic Setup with ioredis
import Redis from 'ioredis';
import { redisStorage } from '@stateloom/persist-redis';
const client = new Redis(); // connects to localhost:6379
const storage = redisStorage({ client });Setup with node-redis
import { createClient } from 'redis';
import { redisStorage } from '@stateloom/persist-redis';
const client = await createClient().connect();
const storage = redisStorage({ client });Setup with Upstash Redis
import { Redis } from '@upstash/redis';
import { redisStorage } from '@stateloom/persist-redis';
const client = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
});
const storage = redisStorage({ client });Custom Key Prefix
By default, all keys are prefixed with stateloom:. Customize with the prefix option:
const storage = redisStorage({
client,
prefix: 'myapp:state:',
});
// Key "user-prefs" becomes "myapp:state:user-prefs" in RedisTTL (Key Expiration)
Set a TTL in seconds to auto-expire keys in Redis:
const storage = redisStorage({
client,
ttl: 3600, // 1 hour
});When ttl is set, keys are stored with a Redis EX argument. Without it, keys persist indefinitely.
Full Integration Example
import Redis from 'ioredis';
import { createStore } from '@stateloom/store';
import { persist } from '@stateloom/persist';
import { redisStorage } from '@stateloom/persist-redis';
const store = createStore(
(set) => ({
theme: 'light' as 'light' | 'dark',
locale: 'en',
notifications: true,
setTheme: (t: 'light' | 'dark') => set({ theme: t }),
setLocale: (l: string) => set({ locale: l }),
}),
{
middleware: [
persist({
key: 'user-prefs',
storage: redisStorage({
client: new Redis(),
prefix: 'myapp:',
ttl: 86400, // 24 hours
}),
partialize: (state) => ({
theme: state.theme,
locale: state.locale,
notifications: state.notifications,
}),
}),
],
},
);API Reference
redisStorage(options: RedisStorageOptions): StorageAdapter
Create a Redis-backed storage adapter.
Parameters:
| Parameter | Type | Description | Default |
|---|---|---|---|
options | RedisStorageOptions | Configuration object. | -- |
options.client | RedisClient | Your Redis client instance. | -- |
options.prefix | string | Key prefix prepended to all storage keys. | 'stateloom:' |
options.ttl | number | TTL in seconds for stored keys. | undefined (no expiry) |
Returns: StorageAdapter -- an async storage adapter for @stateloom/persist.
import Redis from 'ioredis';
import { redisStorage } from '@stateloom/persist-redis';
const storage = redisStorage({
client: new Redis(),
prefix: 'myapp:',
ttl: 3600,
});Key behaviors:
- All operations are async -- the persist middleware handles this via its async hydration path
- Keys are prefixed:
prefix + key(e.g.,stateloom:user-prefs) - With
ttl, keys are set with RedisEXargument - Without
ttl, keys persist indefinitely removeItemuses RedisDELcommand- The adapter has zero runtime dependencies -- it delegates to the provided client
See also: RedisClient, RedisStorageOptions
RedisClient (interface)
Minimal duck-typed Redis client interface. Structurally compatible with ioredis, redis (node-redis), and @upstash/redis.
interface RedisClient {
get(key: string): Promise<string | null>;
set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
del(key: string): Promise<unknown>;
}Any object satisfying this interface works as a client. No specific Redis library is required.
RedisStorageOptions (interface)
interface RedisStorageOptions {
readonly client: RedisClient;
readonly prefix?: string;
readonly ttl?: number;
}| Property | Type | Description | Default |
|---|---|---|---|
client | RedisClient | Redis client instance. | -- |
prefix | string | Key prefix for all operations. | 'stateloom:' |
ttl | number | TTL in seconds. | undefined |
StorageAdapter (interface)
Storage adapter contract matching @stateloom/persist's interface.
interface StorageAdapter {
getItem(key: string): string | null | Promise<string | null>;
setItem(key: string, value: string): void | Promise<void>;
removeItem(key: string): void | Promise<void>;
}Patterns
Per-User Session State
import Redis from 'ioredis';
import { createStore } from '@stateloom/store';
import { persist } from '@stateloom/persist';
import { redisStorage } from '@stateloom/persist-redis';
function createUserStore(userId: string) {
return createStore(
(set) => ({
cart: [] as string[],
addToCart: (item: string) => set((s) => ({ cart: [...s.cart, item] })),
}),
{
middleware: [
persist({
key: `session:${userId}`,
storage: redisStorage({
client: new Redis(),
ttl: 3600,
}),
}),
],
},
);
}Edge Runtime (Upstash)
import { Redis } from '@upstash/redis';
import { persist } from '@stateloom/persist';
import { redisStorage } from '@stateloom/persist-redis';
// Works in Cloudflare Workers, Vercel Edge, Deno Deploy
const storage = redisStorage({
client: new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
}),
prefix: 'edge:',
ttl: 1800,
});Testing with Mock Client
import { describe, it, expect, vi } from 'vitest';
import { redisStorage } from '@stateloom/persist-redis';
it('stores and retrieves values', async () => {
const data = new Map<string, string>();
const mockClient = {
get: vi.fn((key: string) => Promise.resolve(data.get(key) ?? null)),
set: vi.fn((key: string, value: string) => {
data.set(key, value);
return Promise.resolve('OK');
}),
del: vi.fn((key: string) => {
data.delete(key);
return Promise.resolve(1);
}),
};
const storage = redisStorage({ client: mockClient });
await storage.setItem('test', '{"count":1}');
const result = await storage.getItem('test');
expect(result).toBe('{"count":1}');
expect(mockClient.set).toHaveBeenCalledWith('stateloom:test', '{"count":1}');
});How It Works
Storage Adapter Pipeline
The adapter is a thin wrapper that:
- Prepends the
prefixto all keys - Delegates to the client's
get,set, anddelmethods - Adds
'EX', ttlarguments whenttlis configured
Duck-Typed Client
The RedisClient interface uses structural typing. Any object with matching get, set, and del methods works, regardless of the library. This avoids a hard dependency on any specific Redis package.
Async Hydration
All operations return Promise, so the persist middleware uses its async hydration path. State is read from Redis during initialization and written back on each state change.
TypeScript
import { redisStorage } from '@stateloom/persist-redis';
import type { RedisClient, RedisStorageOptions, StorageAdapter } from '@stateloom/persist-redis';
import { expectTypeOf } from 'vitest';
// redisStorage returns StorageAdapter
const mockClient: RedisClient = {
get: async () => null,
set: async () => 'OK',
del: async () => 1,
};
const storage = redisStorage({ client: mockClient });
expectTypeOf(storage).toMatchTypeOf<StorageAdapter>();
// StorageAdapter methods return promises
expectTypeOf(storage.getItem('k')).toEqualTypeOf<string | null | Promise<string | null>>();When to Use
| Scenario | Why @stateloom/persist-redis |
|---|---|
| Server-side state persistence | Redis survives process restarts |
| Per-user session state | Key per user with TTL expiration |
| Edge Runtime (Cloudflare Workers, Vercel Edge) | Upstash HTTP client works in edge environments |
| Multi-server deployments | Shared Redis instance syncs state across servers |
| Testing persist with Redis | Mock client pattern for unit tests |
For client-side persistence, use @stateloom/persist with its built-in browser storage adapters (localStorage, sessionStorage, indexedDB). Use @stateloom/persist-redis when you need server-side or cross-server state persistence.