File

src/common/services/tenant-lookup.service.ts

Description

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.

Index

Methods

Methods

Async getOrgIdBySubdomain
getOrgIdBySubdomain(subdomain: string)
Parameters :
Name Type Optional
subdomain string No
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;
  }
}

results matching ""

    No results matching ""