Skip to main content

Module Registration

forRoot (sync)

Use when your config values are available at module-load time (process env, literals).

IamModule.forRoot({
tenants: { /* … */ },
defaultTenant: 'customer', // optional; auto-picked if only one tenant
global: true, // default true; see below
auditSink: { provide: AUDIT_SINK, useClass: MyAuditSink }, // optional
sessionCache: new InMemorySessionCache(), // optional
});

forRootAsync (async)

Use when config comes from @nestjs/config, a secret manager, or any async source.

// Self-hosted example
IamModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (cs: ConfigService) => ({
tenants: {
customer: {
mode: 'self-hosted',
transport: 'cookie-or-bearer',
trustProxy: true,
kratos: {
publicUrl: cs.getOrThrow('KRATOS_PUBLIC_URL'),
adminUrl: cs.get('KRATOS_ADMIN_URL'),
adminToken: cs.get('KRATOS_ADMIN_TOKEN'),
},
},
},
}),
});

// Ory Cloud example — no kratos block needed; URLs derived from projectSlug.
IamModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (cs: ConfigService) => ({
tenants: {
customer: {
mode: 'cloud',
transport: 'cookie-or-bearer',
trustProxy: true,
cloud: {
projectSlug: cs.getOrThrow('ORY_PROJECT_SLUG'),
apiKey: cs.getOrThrow('ORY_CLOUD_API_KEY'),
},
},
},
}),
});

Config is validated synchronously at module-init time via zod; invalid config fails boot with a descriptive IamConfigurationError listing every offending path. The process exits non-zero — do not catch this.

The global option

global: true (default) registers the full guard chain — SessionGuard, then RoleGuard, then PermissionGuard — as APP_GUARDs, in that order. Every route is authenticated and authorized unless decorated with @Public() / @Anonymous(). RoleGuard and PermissionGuard are no-ops on any route that doesn't carry @RequireRole / @RequirePermission, so global registration is safe for endpoints that only need authentication.

global: false disables all three global bindings — routes default to unauthenticated and you opt in per route via @UseGuards(SessionGuard, RoleGuard, PermissionGuard) (include only what you need). Either way, the module itself is always @Global() in the NestJS sense so guards/services are reachable everywhere.

:::note Since 0.2.0 Before 0.2.0 only SessionGuard was bound to APP_GUARD, which silently turned @RequireRole / @RequirePermission into no-ops unless consumers manually added @UseGuards(RoleGuard, PermissionGuard) to every controller. As of 0.2.0 the three guards run as a chain under APP_GUARD and @RequireRole / @RequirePermission are enforced by default. :::

Tenant config shape

type TenantConfig = {
mode: 'self-hosted' | 'cloud';
transport: 'cookie' | 'bearer' | 'cookie-or-bearer' | 'oathkeeper';

// Required for mode: 'self-hosted'. Optional for mode: 'cloud' —
// the library derives Kratos URLs from cloud.projectSlug and only
// reads this block for overrides (e.g. a project-specific
// sessionCookieName).
kratos?: {
publicUrl?: string; // required in self-hosted; derived from projectSlug in cloud
adminUrl?: string; // required for admin ops (identity CRUD, session revoke)
adminToken?: string; // required when adminUrl is set in self-hosted mode
sessionCookieName?: string; // default 'ory_kratos_session' — override for Ory Cloud
};

// Required for mode: 'cloud'. The library uses these to build the
// Ory Cloud project URL (https://<projectSlug>.projects.oryapis.com)
// and to authenticate admin calls.
cloud?: { projectSlug: string; apiKey: string };

keto?: { readUrl: string; writeUrl: string; apiKey?: string };
hydra?: {
publicUrl: string;
adminUrl: string;
adminToken?: string;
clientId?: string; // required for TokenService.clientCredentials
clientSecret?: string;
};
oathkeeper?: {
identityHeader?: string; // default 'X-User'
signatureHeader?: string; // default 'X-User-Signature'
signerKeys: string[]; // non-empty; allowlist supports rotation
};
logging?: { level: 'error' | 'warn' | 'info' | 'debug' };
cache?: { sessionTtlMs: number; permissionTtlMs: number; jwksTtlMs: number };
trustProxy?: boolean; // required true in production with cookie transport
};

Examples by mode

Cloud

{
mode: 'cloud',
transport: 'cookie-or-bearer',
cloud: {
projectSlug: 'nifty-blackwell-thv46tbvh5',
apiKey: process.env.ORY_CLOUD_API_KEY!,
},
trustProxy: true,
// Optional override: Ory Cloud names the session cookie with a
// project-specific random slug, not the projectSlug used above.
// Look it up in Ory Console → Project Settings → Sessions.
kratos: { sessionCookieName: 'ory_session_abcdef01234' },
}

The library builds the Ory Cloud API URL (https://<projectSlug>.projects.oryapis.com) automatically and uses cloud.apiKey for every admin-scoped call — you do not need to populate kratos.publicUrl, kratos.adminUrl, or kratos.adminToken yourself.

Self-hosted

{
mode: 'self-hosted',
transport: 'cookie-or-bearer',
kratos: {
publicUrl: 'https://kratos.example.com',
adminUrl: 'https://kratos-admin.internal',
adminToken: process.env.KRATOS_ADMIN_TOKEN!,
},
// Optional Keto / Hydra for permissions + OAuth2:
keto: {
readUrl: 'https://keto-read.internal',
writeUrl: 'https://keto-write.internal',
},
hydra: {
publicUrl: 'https://hydra.example.com',
adminUrl: 'https://hydra-admin.internal',
adminToken: process.env.HYDRA_ADMIN_TOKEN!,
clientId: process.env.HYDRA_CLIENT_ID,
clientSecret: process.env.HYDRA_CLIENT_SECRET,
},
trustProxy: true,
}

kratos.publicUrl is required; everything else scales with the features you use (adminUrl/adminToken for admin APIs, keto for permission checks, hydra for OAuth2 / machine-to-machine tokens).

:::note Since 0.2.1 In 0.2.0 the kratos block was required for every tenant regardless of mode, which broke mode: 'cloud' at both the TypeScript (Property 'kratos' is missing in type) and runtime ("kratos is required") layers. 0.2.1 made kratos optional for cloud tenants and derives URLs + admin credentials from cloud.projectSlug + cloud.apiKey. Self-hosted still requires kratos.publicUrl. :::