Aviary
Packages

@dudousxd/nestjs-media-upload-redis

A Redis-backed UploadSessionStore — share resumable (tus/proxy) upload sessions across instances.

A Redis adapter for the UploadSessionStore SPI. The bundled InMemoryUploadSessionStore is fine for a single process, but behind a load balancer a resumed chunk can land on a different node than the one that created the session. Back the sessions with Redis and any instance can pick up where another left off.

Proxy path only

This store is for the resumable proxy (tus) path, where your backend tracks each upload's resume offset. The direct presigned-multipart path is stateless on your side (S3 holds the in-progress upload), so it needs no session store.

Install & wire

app.module.ts
import { MediaModule } from '@dudousxd/nestjs-media';
import { RedisUploadSessionStore } from '@dudousxd/nestjs-media-upload-redis';
import Redis from 'ioredis';

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

MediaModule.forRoot({
  default: 's3',
  disks: { s3 },
  uploadSessions: new RedisUploadSessionStore(redis),
  tus: { disk: 's3', basePath: '/media/uploads' },
});

forRootAsync keeps the Redis client in DI — resolve it from a provider rather than constructing it inline:

app.module.ts
MediaModule.forRootAsync({
  inject: ['REDIS'],
  useFactory: (redis: Redis) => ({
    default: 's3',
    disks: { s3 },
    uploadSessions: new RedisUploadSessionStore(redis, { keyPrefix: 'app:upload', ttlSeconds: 3600 }),
    tus: { disk: 's3' },
  }),
});

The client contract

The store accepts any object satisfying the minimal MinimalRedis shape, so it works with ioredis, redis (node-redis v4), or a thin wrapper — no specific client is a dependency:

interface MinimalRedis {
  get(key: string): Promise<string | null>;
  set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
  del(key: string): Promise<unknown>;
}

Sessions are stored as JSON under ${keyPrefix}:${id} and written with an EX TTL on every create/update, so abandoned uploads expire on their own.

Options

new RedisUploadSessionStore(redis, {
  keyPrefix: 'media:upload:session', // default
  ttlSeconds: 86400,                 // default — 24h sliding expiry, refreshed on each write
});

get validates the stored shape before returning it (null on a corrupt or missing entry), so a manually-flushed key never crashes a resume.

Peer dependencies

@dudousxd/nestjs-media-core only — your Redis client is yours to provide.

On this page