Skip to content

Session

Contract

@modularityjs/session defines the abstract SessionService:

typescript
interface SessionSetOptions {
  readonly ttlMs?: number;
}

abstract class SessionService {
  abstract get(id: string): Promise<SessionData | undefined>;
  abstract set(
    id: string,
    data: SessionData,
    options?: SessionSetOptions,
  ): Promise<void>;
  abstract destroy(id: string): Promise<void>;
  abstract destroyForIdentity(identityId: string): Promise<void>;
  abstract regenerate(id: string): Promise<string>;
}

interface SessionData {
  readonly id: string;
  readonly data: Record<string, unknown>;
  readonly createdAt: number;
  readonly updatedAt: number;
}

SessionModule provides a SessionConfig with a configurable TTL in milliseconds (default: 86_400_000 / 24 hours):

typescript
import { SessionModule } from '@modularityjs/session';

SessionModule.forRoot({ ttlMs: 3_600_000 }); // 1 hour

A per-call set(id, data, { ttlMs }) overrides the default for that entry. set is a template method on the contract: when ttlMs is provided it validates it (rejecting a non-positive or non-integer value with a ValidationException) before delegating to the driver, so the memory and Redis drivers agree instead of one accepting what the other rejects. Omit ttlMs to use the configured default.

Drivers

Memory (@modularityjs/session-memory)

Map-based in-memory session store with TTL expiration. For development and testing.

typescript
import { SessionModule } from '@modularityjs/session';
import { SessionMemoryModule } from '@modularityjs/session-memory';

const modules = [SessionModule, SessionMemoryModule];

Redis (@modularityjs/session-redis)

Redis-backed session store with automatic expiration via SET EX. Requires @modularityjs/redis.

typescript
import { SessionModule } from '@modularityjs/session';
import { SessionRedisModule } from '@modularityjs/session-redis';
import { RedisModule } from '@modularityjs/redis';

const modules = [
  RedisModule.forRoot({ url: 'redis://localhost:6379' }),
  SessionModule.forRoot({ ttlMs: 3_600_000 }),
  SessionRedisModule,
  // or with config:
  SessionRedisModule.forRoot({ keyNamespace: 'sess:' }),
];

TypeORM (@modularityjs/session-database-typeorm)

Sessions in a SQL table, for a deployment that runs a database but no Redis. The module registers the canonical SessionRow entity (session table: id, identity_id, payload, expires_at) into DatabaseEntitiesPool and binds TypeormSessionService as the SessionService preference. Parameterless — the TTL still comes from SessionModule.forRoot({ ttlMs }).

typescript
import { DatabaseModule } from '@modularityjs/database';
import { DatabaseTypeormModule } from '@modularityjs/database-typeorm';
import { SessionModule } from '@modularityjs/session';
import { SessionDatabaseTypeormModule } from '@modularityjs/session-database-typeorm';

const modules = [
  DatabaseModule,
  DatabaseTypeormModule.forRoot({/* ... */}),
  SessionModule.forRoot({ ttlMs: 3_600_000 }),
  SessionDatabaseTypeormModule,
];

Prisma (@modularityjs/session-database-prisma)

The same store over a Prisma model delegate. SessionPrismaConfig.model names the delegate property on your client (default 'session'):

prisma
model Session {
  id         String   @id
  identityId String?
  payload    String
  expiresAt  DateTime

  @@index([identityId])
  @@index([expiresAt])
}
typescript
import { DatabasePrismaModule } from '@modularityjs/database-prisma';
import { SessionModule } from '@modularityjs/session';
import { SessionDatabasePrismaModule } from '@modularityjs/session-database-prisma';

const modules = [
  DatabasePrismaModule.forRoot({ clientFactory: () => prisma }),
  SessionModule.forRoot({ ttlMs: 3_600_000 }),
  SessionDatabasePrismaModule,
  // or, when the model is named differently:
  SessionDatabasePrismaModule.forRoot({ model: 'userSession' }),
];

Expiry and purging

Redis expires session keys for you; a relational table does not. Both database stores enforce expiry on read — every lookup filters on the expiry column, so a row past expiresAt is never returned, never regenerated, and is overwritten in place by a later set on the same id. Nothing deletes it at that moment, so expired rows accumulate.

TypeormSessionService.purgeExpired() / PrismaSessionService.purgeExpired() is the seam: it deletes every session past its expiry and returns the count removed. Wire it to a scheduler job — the modules deliberately start no timer, so the schedule stays a decision your app declares:

typescript
import { Inject, Injectable } from '@modularityjs/di';
import type { ScheduledJob } from '@modularityjs/scheduler';
import { ScheduledJobsPool, SchedulerModule } from '@modularityjs/scheduler';
import { Module } from '@modularityjs/modularity';
import {
  SessionDatabaseTypeormModule,
  TypeormSessionService,
} from '@modularityjs/session-database-typeorm';

@Injectable()
class PurgeExpiredSessionsJob implements ScheduledJob {
  readonly name = 'purge-expired-sessions';
  readonly schedule = '*/15 * * * *';

  constructor(
    @Inject(TypeormSessionService)
    private readonly sessions: TypeormSessionService,
  ) {}

  async execute(): Promise<void> {
    await this.sessions.purgeExpired();
  }
}

@Module({
  name: 'session-janitor',
  imports: [SchedulerModule, SessionDatabaseTypeormModule],
  providers: [PurgeExpiredSessionsJob],
  pools: [
    {
      pool: ScheduledJobsPool,
      key: 'purge-expired-sessions',
      useClass: PurgeExpiredSessionsJob,
    },
  ],
})
class SessionJanitorModule {}

Identity-wide revocation

destroyForIdentity(identityId) — "log out everywhere", and the revocation a password change owes the user — is a single indexed DELETE ... WHERE identity_id = ? on both stores. identityId is a real column written by the same statement that writes the payload, so unlike the Redis driver (whose identity index is a candidate list it must re-verify against each session before deleting) the SQL delete is authoritative on its own.

HTTP Integration

@modularityjs/http-session wires session management into HTTP requests through the abstract HttpServer contract — it reads the session id from request.cookies and writes the cookie on the response, with no driver-specific imports. The underlying Fastify driver provides cookie parsing/serialisation via @fastify/cookie.

typescript
import { HttpModule } from '@modularityjs/http';
import { SessionModule } from '@modularityjs/session';
import { HttpFastifyModule } from '@modularityjs/http-fastify';
import { SessionMemoryModule } from '@modularityjs/session-memory';
import { HttpSessionModule } from '@modularityjs/http-session';

const modules = [
  HttpModule.forRoot({ port: 3000 }),
  HttpFastifyModule,
  SessionModule.forRoot({ ttlMs: 3_600_000 }),
  SessionMemoryModule,
  HttpSessionModule,
  // or with config:
  HttpSessionModule.forRoot({
    cookieName: 'my_sid',
    cookie: { secure: true, sameSite: 'strict' },
  }),
];

@Session() Decorator

Use the @Session() parameter decorator in controllers to access session data:

typescript
import { Inject, Injectable } from '@modularityjs/di';
import { Get } from '@modularityjs/http';
import { Session } from '@modularityjs/http-session';
import type { SessionData } from '@modularityjs/session';

@Injectable()
class ProfileController {
  @Get('/profile')
  getProfile(@Session() session: SessionData) {
    return { sessionId: session.id, data: session.data };
  }
}

HttpSessionConfig controls cookie behavior. cookieName is top-level; the rest nest under cookie:

OptionDefaultDescription
cookieName'sid'Name of the session cookie
cookie.httpOnlytruePrevents client-side JavaScript access
cookie.securetrueRequires HTTPS (override to false for local HTTP)
cookie.sameSite'lax'CSRF protection (strict, lax, none)
cookie.path'/'Cookie path
cookie.domainCookie domain (omitted by default)
rollingfalseReset cookie expiration on every response

Usage

typescript
import { Inject, Injectable } from '@modularityjs/di';
import { SessionService } from '@modularityjs/session';

@Injectable()
class AuthService {
  constructor(
    @Inject(SessionService) private readonly session: SessionService,
  ) {}

  async login(userId: string): Promise<string> {
    const now = Date.now();
    const sessionData = {
      id: crypto.randomUUID(),
      data: { userId },
      createdAt: now,
      updatedAt: now,
    };
    await this.session.set(sessionData.id, sessionData);
    return sessionData.id;
  }

  async logout(sessionId: string): Promise<void> {
    await this.session.destroy(sessionId);
  }
}