Skip to content

@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 ​

bash
pnpm add @stateloom/persist @stateloom/persist-redis
bash
npm install @stateloom/persist @stateloom/persist-redis
bash
yarn add @stateloom/persist @stateloom/persist-redis

Size: ~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 ​

typescript
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 ​

typescript
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 ​

typescript
import { createClient } from 'redis';
import { redisStorage } from '@stateloom/persist-redis';

const client = await createClient().connect();
const storage = redisStorage({ client });

Setup with Upstash Redis ​

typescript
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:

typescript
const storage = redisStorage({
  client,
  prefix: 'myapp:state:',
});
// Key "user-prefs" becomes "myapp:state:user-prefs" in Redis

TTL (Key Expiration) ​

Set a TTL in seconds to auto-expire keys in Redis:

typescript
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 ​

typescript
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:

ParameterTypeDescriptionDefault
optionsRedisStorageOptionsConfiguration object.--
options.clientRedisClientYour Redis client instance.--
options.prefixstringKey prefix prepended to all storage keys.'stateloom:'
options.ttlnumberTTL in seconds for stored keys.undefined (no expiry)

Returns: StorageAdapter -- an async storage adapter for @stateloom/persist.

typescript
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 Redis EX argument
  • Without ttl, keys persist indefinitely
  • removeItem uses Redis DEL command
  • 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.

typescript
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) ​

typescript
interface RedisStorageOptions {
  readonly client: RedisClient;
  readonly prefix?: string;
  readonly ttl?: number;
}
PropertyTypeDescriptionDefault
clientRedisClientRedis client instance.--
prefixstringKey prefix for all operations.'stateloom:'
ttlnumberTTL in seconds.undefined

StorageAdapter (interface) ​

Storage adapter contract matching @stateloom/persist's interface.

typescript
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 ​

typescript
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) ​

typescript
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 ​

typescript
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:

  1. Prepends the prefix to all keys
  2. Delegates to the client's get, set, and del methods
  3. Adds 'EX', ttl arguments when ttl is 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 ​

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 ​

ScenarioWhy @stateloom/persist-redis
Server-side state persistenceRedis survives process restarts
Per-user session stateKey per user with TTL expiration
Edge Runtime (Cloudflare Workers, Vercel Edge)Upstash HTTP client works in edge environments
Multi-server deploymentsShared Redis instance syncs state across servers
Testing persist with RedisMock 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.