src/common/services/tenant-lookup.service.ts
Tenant subdomain -> organizationId lookup.
Uses its own raw PrismaClient (NOT the RLS proxy) so it can query the
organizations table before any tenant context exists. The lookup runs
inside the TenantHeaderGuard, which executes BEFORE the
TenantContextInterceptor sets the GUC, so the RLS proxy wouldn't have
a tx to route to.
organizations is intentionally NOT under RLS (see prisma.service.ts
docstring) so a raw client can read it without side effects.
Results are cached in-memory for 60s to avoid hitting the DB on every request. That's acceptable because org subdomains rarely change.
Methods |
|
| Async getOrgIdBySubdomain | ||||||
getOrgIdBySubdomain(subdomain: string)
|
||||||
|
Parameters :
Returns :
Promise<string | null>
|
| Async onModuleDestroy |
onModuleDestroy()
|
|
Returns :
any
|
| Async onModuleInit |
onModuleInit()
|
|
Returns :
any
|
import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
/**
* Tenant subdomain -> organizationId lookup.
*
* Uses its own raw PrismaClient (NOT the RLS proxy) so it can query the
* `organizations` table before any tenant context exists. The lookup runs
* inside the TenantHeaderGuard, which executes BEFORE the
* TenantContextInterceptor sets the GUC, so the RLS proxy wouldn't have
* a tx to route to.
*
* `organizations` is intentionally NOT under RLS (see prisma.service.ts
* docstring) so a raw client can read it without side effects.
*
* Results are cached in-memory for 60s to avoid hitting the DB on every
* request. That's acceptable because org subdomains rarely change.
*/
@Injectable()
export class TenantLookupService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(TenantLookupService.name);
private readonly prisma = new PrismaClient();
private readonly cache = new Map<string, { orgId: string; expiresAt: number }>();
private readonly ttlMs = 60_000;
async onModuleInit() {
await this.prisma.$connect();
this.logger.log('TenantLookupService connected');
}
async onModuleDestroy() {
await this.prisma.$disconnect();
}
async getOrgIdBySubdomain(subdomain: string): Promise<string | null> {
const key = subdomain.toLowerCase().trim();
const cached = this.cache.get(key);
if (cached && cached.expiresAt > Date.now()) {
return cached.orgId;
}
const org = await this.prisma.organization.findFirst({
where: { subdomain: key },
select: { id: true },
});
if (!org) return null;
this.cache.set(key, { orgId: org.id, expiresAt: Date.now() + this.ttlMs });
return org.id;
}
}