Session
Contract
@modularityjs/session defines the abstract SessionService:
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):
import { SessionModule } from '@modularityjs/session';
SessionModule.forRoot({ ttlMs: 3_600_000 }); // 1 hourA 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.
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.
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 }).
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'):
model Session {
id String @id
identityId String?
payload String
expiresAt DateTime
@@index([identityId])
@@index([expiresAt])
}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:
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.
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:
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 };
}
}Cookie Options
HttpSessionConfig controls cookie behavior. cookieName is top-level; the rest nest under cookie:
| Option | Default | Description |
|---|---|---|
cookieName | 'sid' | Name of the session cookie |
cookie.httpOnly | true | Prevents client-side JavaScript access |
cookie.secure | true | Requires HTTPS (override to false for local HTTP) |
cookie.sameSite | 'lax' | CSRF protection (strict, lax, none) |
cookie.path | '/' | Cookie path |
cookie.domain | — | Cookie domain (omitted by default) |
rolling | false | Reset cookie expiration on every response |
Usage
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);
}
}