Skip to content

Configuration ​

Options ​

OptionTypeDefaultDescription
isGlobalbooleanfalseMake the module global
clientstring'default'Named Redis client to use
keyPrefixstring'sess:'Redis key namespace
defaultTtlMsnumber86_400_000 (1 day)TTL used when the middleware provides no cookie expiry
userIdExtractor(session: unknown) => string | undefinedreads session.passport.userExtracts the owning user's ID from the raw payload; sessions without an ID are stored but not indexed per-user
absoluteLifetimeMsnumberundefined (off)Absolute lifetime cap — see Security Policies
maxSessionsPerUsernumberundefined (off)Per-user seat limit
maxSessionsPolicy'reject' | 'evict-oldest''evict-oldest'What happens at the seat limit
eventsISessionEventsundefinedLifecycle callbacks — see Monitoring

Configuration is validated fail-fast at bootstrap: an invalid knob throws InvalidSessionConfigError before the store is ever used.

Synchronous Configuration ​

typescript
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-redisx/core';
import { SessionPlugin } from '@nestjs-redisx/session';

@Module({
  imports: [
    RedisModule.forRoot({
      clients: {
        host: 'localhost',
        port: 6379,
      },
      plugins: [
        new SessionPlugin({
          keyPrefix: 'sess:', // Redis key namespace
          defaultTtlMs: 86_400_000, // 1 day when the cookie has no maxAge
        }),
      ],
    }),
  ],
})
export class AppModule {}

Async Configuration ​

SessionPlugin.registerAsync() follows the standard NestJS RedisX pattern — the plugin instance stays outside useFactory, the factory produces plugin options:

typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisModule } from '@nestjs-redisx/core';
import { SessionPlugin } from '@nestjs-redisx/session';

@Module({
  imports: [
    RedisModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      plugins: [
        SessionPlugin.registerAsync({
          imports: [ConfigModule],
          inject: [ConfigService],
          useFactory: (config: ConfigService) => ({
            maxSessionsPerUser: config.get<number>('MAX_SESSIONS_PER_USER', 5),
            absoluteLifetimeMs: config.get<number>('SESSION_ABSOLUTE_LIFETIME_MS', 12 * 3600 * 1000),
          }),
        }),
      ],
      useFactory: (config: ConfigService) => ({
        clients: {
          host: config.get<string>('REDIS_HOST', 'localhost'),
          port: config.get<number>('REDIS_PORT', 6379),
        },
      }),
    }),
  ],
})
export class AppModule {}

Custom User ID Extraction ​

The default extractor reads the Passport convention (session.passport.user). For custom auth, provide your own:

typescript
new SessionPlugin({
  userIdExtractor: (session) => (session as MySession).auth?.accountId,
});

Sessions for which the extractor returns undefined (e.g. anonymous carts) are stored normally but skipped by per-user indexing, counting, and limits.

Named Clients ​

Like every RedisX plugin, the session store can run on a dedicated connection:

typescript
RedisModule.forRoot({
  clients: {
    default: { host: 'localhost', port: 6379 },
    sessions: { host: 'localhost', port: 6379, db: 1 },
  },
  plugins: [new SessionPlugin({ client: 'sessions' })],
});

Redis Keyspace ​

KeyTypeContents
sess:{<sid>}STRINGMiddleware payload as JSON, PX = session TTL
sess:{<sid>}:metaHASHuserId, ip, userAgent, createdAt, lastSeenAt, expiresAt — same TTL, same cluster slot (hash tag)
sess:user:<userId>ZSETsid -> expiresAtMs, expired members swept lazily by score
sess:indexZSETGlobal sid -> expiresAtMs index behind count()

Migration from connect-redis

The payload key includes a hash tag (sess:{abc123} rather than sess:abc123) so payload+metadata operations stay atomic on Redis Cluster. Existing connect-redis keys are therefore not picked up — users sign in again once at cutover.

Released under the MIT License.