Skip to content

@stratum-hq/nestjs

@stratum-hq/nestjs provides first-class NestJS integration for Stratum. It includes a guard that resolves tenants from incoming requests, a parameter decorator for extracting tenant context, and a module for dependency injection.

npm install @stratum-hq/nestjs @stratum-hq/sdk @stratum-hq/core

Peer dependencies: @nestjs/common >= 10, @nestjs/core >= 10, reflect-metadata.

import { Module } from "@nestjs/common";
import { StratumModule } from "@stratum-hq/nestjs";
// Without jwtSecret, the guard does not verify tokens and reads the tenant
// from the X-Tenant-ID header, which any client can send.
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
throw new Error("JWT_SECRET is required to verify bearer tokens.");
}
@Module({
imports: [
StratumModule.forRoot({
controlPlaneUrl: "http://localhost:3001",
apiKey: "sk_live_your_key",
jwtSecret, // optional: enables JWT verification
jwtClaimPath: "tenant_id",
}),
],
})
export class AppModule {}

Then use the guard and decorator in your controllers:

import { Controller, Get, UseGuards } from "@nestjs/common";
import type { ResolvedTenantContext } from "@stratum-hq/core";
import { StratumGuard, Tenant } from "@stratum-hq/nestjs";
@Controller("data")
@UseGuards(StratumGuard)
export class DataController {
@Get()
getData(@Tenant() tenant: ResolvedTenantContext) {
return {
tenantId: tenant.tenant_id,
config: tenant.resolved_config,
};
}
}
// Without jwtSecret, the guard does not verify tokens and reads the tenant
// from the X-Tenant-ID header, which any client can send.
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
throw new Error("JWT_SECRET is required to verify bearer tokens.");
}
StratumModule.forRoot({
controlPlaneUrl: "http://localhost:3001",
apiKey: "sk_live_your_key",
jwtSecret, // Optional: enables JWT verification
jwtClaimPath: "tenant_id", // Optional: JWT claim path
})

For configuration that depends on other providers (e.g., ConfigService):

StratumModule.forRootAsync({
useFactory: (config: ConfigService) => {
const jwtSecret = config.get("JWT_SECRET");
if (!jwtSecret) {
throw new Error("JWT_SECRET is required to verify bearer tokens.");
}
return {
controlPlaneUrl: config.get("STRATUM_URL"),
apiKey: config.get("STRATUM_API_KEY"),
jwtSecret,
};
},
inject: [ConfigService],
})

The module is @Global(), so you only need to import it once in your root module.

The guard resolves tenant context from incoming requests using the same resolution chain as the Express/Fastify middleware:

  1. JWT: extracts tenant ID from a verified Bearer token claim (requires jwtSecret or jwtVerify)
  2. Header: reads X-Tenant-ID header (or custom headerName). When JWT verification is configured, this step runs only with trustTenantHeader: true.
  3. Custom resolvers: your async functions, tried in order

When JWT verification is configured, a Bearer token that fails verification gets UnauthorizedException (HTTP 401), and the guard does not try the header or the resolvers.

If the request has no tenant ID, the guard throws UnauthorizedException (HTTP 401). If the tenant does not exist, it throws NotFoundException (HTTP 404). If the tenant is suspended, or the control plane denies access to it, the guard throws ForbiddenException (HTTP 403). If the tenant is archived, it throws GoneException (HTTP 410). If the control plane does not respond within 10 seconds, the SDK’s default time limit, it throws GatewayTimeoutException (HTTP 504). If the control plane rejects the SDK API key, it throws InternalServerErrorException (HTTP 500) and writes the cause to console.error. The same rules apply to an impersonation target. These statuses match the Express and Fastify middleware in @stratum-hq/sdk.

@UseGuards(StratumGuard)
@Controller("api")
export class ApiController {
// All routes require a valid tenant
}
Property Type Description
req.tenant ResolvedTenantContext Full resolved context (config, permissions)
req.impersonating boolean Whether this is an impersonated request
req.originalTenantId string? Original tenant ID if impersonating

Parameter decorator that extracts req.tenant from the request:

@Get("profile")
getProfile(@Tenant() tenant: any) {
return {
id: tenant.tenant_id,
maxUsers: tenant.resolved_config["max_users"]?.value,
};
}

If impersonation is configured in the module options, the guard checks for the X-Impersonate-Tenant header and resolves the impersonated tenant’s context:

StratumModule.forRoot({
controlPlaneUrl: "http://localhost:3001",
apiKey: "sk_live_your_key",
impersonation: {
enabled: true,
authorize: async (req, fromTenantId, toTenantId) => {
// Return true if the caller is allowed to impersonate
return isAdmin(req);
},
},
})

Add custom tenant resolution logic (e.g., subdomain-based):

StratumModule.forRoot({
controlPlaneUrl: "http://localhost:3001",
apiKey: "sk_live_your_key",
resolvers: [
{
resolve: async (req) => {
const subdomain = req.hostname?.split(".")[0];
return subdomain !== "www" ? subdomain : null;
},
},
],
})

The guard binds tenant context to AsyncLocalStorage, so downstream services can access it without the request object:

import { getTenantContext } from "@stratum-hq/sdk";
@Injectable()
export class OrderService {
async getOrders() {
const ctx = getTenantContext(); // works without req
// Use ctx.tenant_id, ctx.resolved_config, etc.
}
}