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.