WisarWisar
Dasturlash kitobi/8-QISM — NestJS30 daqiqa

8.25-bob: Multi-tenancy (SaaS)

8-QISM — NestJS (chuqur) · 25-mavzu · Amaliy real mavzu


1. Kirish va motivatsiya

Endi zamonaviy biznes modelining — SaaS (Software as a Service) — texnik asosini, multi-tenancy ni o'rganamiz. SaaS — bir ilovani ko'p mijoz-kompaniyaga (tenant — ijarachi) xizmat sifatida sotish: bitta kodbaza, bitta deploy, lekin har kompaniya o'z ma'lumoti bilan ishlaydi (CRM, hisobot tizimi, do'kon platformasi, bron tizimi). Misol: bir CRM ilovasidan "Mana kompaniya" ham, "Olma kompaniya" ham foydalanadi — lekin biri ikkinchisining mijozlarini ko'rmasligi kerak. Bu — eng ko'p qo'shimcha pul keltiradigan model (oylik obuna — 8.29) va eng ko'p so'raladigan loyiha turi.

Multi-tenancy (ko'p ijarachilik) — bir tizimda ko'p tenant (mijoz-kompaniya) ma'lumotini izolyatsiya bilan saqlash. Eng muhim, eng xavfli masala — ma'lumot izolyatsiyasi: bir tenant hech qachon boshqasining ma'lumotini ko'rmasligi kerak (bir xato — falokat: mijoz raqibining ma'lumotini ko'radi — biznes tugaydi, sud). Shuning uchun multi-tenancy — diqqat va to'g'ri arxitektura talab qiladi. Uch asosiy strategiya bor (shared schema, schema-per-tenant, database-per-tenant), har birining trade-off'i.

Bu bob: SaaS va multi-tenancy nima, tenant aniqlash (subdomain/header/JWT), 3 izolyatsiya strategiyasi (shared schema + tenant_id, schema-per-tenant, database-per-tenant — qachon qaysi), avtomatik tenant filtri (eng muhim — har so'rovga), Row-Level Security (RLS — DB darajasida), tenant kontekst (NestJS), onboarding (yangi tenant), va xavfsizlik (izolyatsiya buzilmasligi). Bu bob 8.3 (DB), 8.6 (guard/middleware), 8.9 (auth), 8.29 (subscription) bilan bog'liq. Multi-tenancy — SaaS qurishning poydevori.

O'xshatish: multi-tenancy — biznes-markaz binosi. Bitta bino (ilova, infratuzilma), lekin ko'p kompaniya ijaraga oladi (tenant). Uch model: shared — barcha kompaniya bitta katta ochiq ofisda, har stol egasining bayrog'i bilan belgilangan (tenant_id — eng arzon, lekin chegaralar nozik); schema-per-tenant — har kompaniyaga alohida xona (alohida schema — o'rtacha); database-per-tenant — har kompaniyaga alohida bino qavati (alohida DB — eng xavfsiz, eng qimmat). Eng muhim qoida: bir kompaniya xodimi hech qachon boshqa kompaniya stoliga o'tirmasligi (ma'lumot izolyatsiyasi) — buni qo'riqchi (guard/RLS) ta'minlaydi.

Nega muhim?

  • SaaS poydevori — eng ko'p pul keltiradigan model (obuna — 8.29).
  • Eng so'raladigan loyiha — CRM, hisobot, platforma (B2B).
  • Ma'lumot izolyatsiyasi — eng xavfli masala (xato = falokat).
  • Arxitektura qarori — strategiya tanlovi (trade-off).

2. Nazariya — chuqur tushuntirish

2.1. SaaS va tenant tushunchasi

text
  SaaS — dasturni XIZMAT sifatida sotish (oylik obuna — 8.29)
  TENANT — bir mijoz-kompaniya (ijarachi)

  Bitta ilova:
  ├── Tenant A (Mana kompaniya) — o'z mijozlari, mahsulotlari
  ├── Tenant B (Olma kompaniya) — o'z ma'lumoti (A'ni KO'RMAYDI)
  └── Tenant C (...)

   Har tenant IZOLYATSIYALANGAN (bir-birini ko'rmaydi)
   Bitta kodbaza, bitta deploy (yangilanish — hammaga)

SaaS/tenant: SaaS — dastur xizmat sifatida (obuna — 8.29); tenant — bir mijoz-kompaniya (ijarachi). Bitta ilova ko'p tenant'ga xizmat qiladi, har biri izolyatsiyalangan (bir-birini ko'rmaydi). Bitta kodbaza/deploy (yangilanish hammaga bir vaqtda — afzallik). Tenant ≠ user: bir tenant'da ko'p user (kompaniya xodimlari). Multi-tenancy — shu izolyatsiyani ta'minlash.

2.2. Tenant aniqlash (qaysi tenant)

text
  HAR SO'ROVDA — qaysi tenant ekanini aniqlash:

  1. Subdomain: mana.app.uz, olma.app.uz  tenant = subdomain (eng keng)
  2. Custom domain: crm.mana.uz  DB'dan domen  tenant
  3. Header: X-Tenant-ID: mana (API uchun)
  4. JWT: token ichida tenantId (login'da belgilanadi — 8.9)
  5. Path: app.uz/mana/... (kamroq)

   Eng keng: subdomain (UI) + JWT tenantId (auth)

Amalda bir nechta manba birga uchraydi — shuning uchun ustuvorlik tartibi (precedence) muhim. Ishonchli manba (JWT — imzolangan, soxtalashtirib bo'lmaydi) soxtalashtiriladigan manbadan (URL, header) ustun turishi kerak. Custom domen holatida esa domen tenant xaritasi DB'dan (yoki cache'dan) qidiriladi:

typescript
// Tenant aniqlovchi — bir nechta manbani ustuvorlik bilan tekshiradi
@Injectable()
export class TenantResolver {
  constructor(private tenantRepo: TenantRepository, private cache: CacheService) {}

  async resolve(req: Request): Promise<string> {
    // 1) JWT — ENG ishonchli (imzolangan — 8.9). Bo'lsa, shu yakuniy.
    if (req.user?.tenantId) return req.user.tenantId;

    const host = req.hostname;                          // masalan: mana.app.uz | crm.mana.uz

    // 2) Subdomain — asosiy domenning bir qismi bo'lsa (mana.app.uz  "mana")
    if (host.endsWith(".app.uz")) {
      const subdomain = host.split(".")[0];
      const t = await this.tenantBySubdomain(subdomain);
      if (t) return t.id;
    }

    // 3) Custom domen — mijozning o'z domeni (crm.mana.uz)  DB xaritasi (cache bilan)
    const byDomain = await this.cache.wrap(`domain:${host}`, () =>
      this.tenantRepo.findByCustomDomain(host),
    );
    if (byDomain) return byDomain.id;

    // 4) Header — faqat server-server API uchun (mijoz brauzeri emas — soxtalashtiriladi)
    const header = req.header("X-Tenant-ID");
    if (header && (await this.tenantRepo.findOneBy({ id: header }))) return header;

    throw new BadRequestException("Tenant aniqlanmadi");   // hech biri  rad
  }

  private tenantBySubdomain(sub: string) {
    return this.cache.wrap(`sub:${sub}`, () => this.tenantRepo.findBySubdomain(sub));
  }
}

Tenant aniqlash (har so'rovda — qaysi tenant): subdomain (mana.app.uz — eng keng, toza), custom domain (crm.mana.uz DB/cache'dan domentenant xaritasi — white-label mijoz o'z domenidan kiradi), header (X-Tenant-ID — faqat API/server-server), JWT (token ichida tenantId — login'da — 8.9), path (app.uz/mana/... — kamroq, chalkash). Ustuvorlik tartibi kritik: JWT (imzolangan — ishonchli) URL/header (soxtalashtiriladigan) dan ustun — token bilan kirgan foydalanuvchi X-Tenant-ID header'ini almashtirib boshqa tenant'ga o'tolmasligi kerak. Custom domen qidiruvini cache'lang (har so'rovda DB'ga bormaslik — 6.16, cache kaliti domen bo'yicha, tenant_id bilan chalkashmaydi). Tenant aniqlanmasa — rad. Bu — har so'rovning birinchi qadami (keyin shu tenant ma'lumoti bilan ishlash).

2.3. 3 izolyatsiya strategiyasi (qachon qaysi)

text
  ┌──────────────────────┬────────────────────────────────────────┐
  │ 1. Shared schema     │ Bitta DB, bitta schema, tenant_id ustun│
  │    (+ tenant_id)     │ Har jadvalda tenant_id. Filtr bilan.   │
  │                      │  Arzon, oddiy, oson masshtab          │
  │                      │  Izolyatsiya app/RLS'ga bog'liq       │
  │                      │  MVP, ko'p kichik tenant (eng keng)    │
  ├──────────────────────┼────────────────────────────────────────┤
  │ 2. Schema-per-tenant │ Bitta DB, har tenant alohida schema    │
  │                      │  Yaxshi izolyatsiya, o'rtacha narx    │
  │                      │  Migration murakkab (har schema)      │
  ├──────────────────────┼────────────────────────────────────────┤
  │ 3. Database-per-     │ Har tenant alohida DB                  │
  │    tenant            │  Maksimal izolyatsiya (compliance)    │
  │                      │  Qimmat, masshtab qiyin               │
  │                      │  enterprise, qattiq talab             │
  └──────────────────────┴────────────────────────────────────────┘

3 strategiya: shared schema + tenant_id (bitta DB, har jadvalda tenant_id ustun, filtr bilan — arzon, oddiy, eng keng — MVP/ko'p kichik tenant); schema-per-tenant (bitta DB, har tenant schema — o'rtacha izolyatsiya/narx, migration murakkab); database-per-tenant (har tenant DB — maksimal izolyatsiya, qimmat — enterprise/compliance). Ko'p SaaS shared schema + RLS dan boshlaydi (kuchli izolyatsiya, arzon), keyin katta tenant'ni alohida DB'ga ko'chiradi (hybrid). Tanlov — narx vs izolyatsiya.

2.4. Shared schema + tenant_id (eng keng)

typescript
// Har jadvalda tenant_id ustun
@Entity()
export class Customer {
  @PrimaryGeneratedColumn("uuid") id: string;
  @Column() @Index() tenantId: string;                // QAYSI tenant (indeks!)
  @Column() ism: string;
  @Column() telefon: string;
}

//  HAR so'rovda tenant_id filtri (eng muhim — 2.5)
async mijozlar(tenantId: string) {
  return this.repo.find({ where: { tenantId } });     // FAQAT shu tenant
}

Shared schema + tenant_id: har jadvalda tenantId ustun (qaysi tenant — indeks bilan — tez filtr). Har so'rovda tenantId filtri (eng muhim, eng xavfli — unutilsa, boshqa tenant ma'lumoti ko'rinadi!). Bu — eng keng strategiya (arzon, oddiy). Lekin xavf: filtr unutilishi mumkin (inson xatosi) avtomatik filtr 2.5-bob yoki RLS 2.7-bob zarur. Qo'lda filtr — ishonchsiz.

2.5. Avtomatik tenant filtri (eng muhim)

typescript
// MUAMMO: har so'rovda qo'lda tenant_id — unutiladi (xavf!)
// YECHIM: avtomatik (har so'rovga tenant_id qo'shiladi)

// TypeORM — global scope / repository wrapper
@Injectable()
export class TenantAwareRepository<T> {
  constructor(private repo: Repository<T>, private tenant: TenantContext) {}

  find(options: any = {}) {
    return this.repo.find({
      ...options,
      where: { ...options.where, tenantId: this.tenant.tenantId },   // AVTOMATIK
    });
  }
  // create, update, delete — barchasi tenant_id bilan
}

Avtomatik tenant filtri (multi-tenancy'ning kaliti): qo'lda tenantId filtri unutiladi (inson xatosi — bir unutish = ma'lumot oqishi). Yechim: avtomatik (har so'rovga tenant_id qo'shiladi — repository wrapper, TypeORM subscriber, yoki Prisma extension). Dasturchi tenant_id'ni o'ylamaydi (avtomatik). Eng ishonchli — RLS (DB darajasida — 2.7). Avtomatik filtr — izolyatsiyani kafolatlaydi (qo'lga ishonmaslik).

2.6. Tenant kontekst (NestJS — har so'rovda)

typescript
// Tenant kontekst — joriy so'rovning tenant'i (request-scoped)
@Injectable()
export class TenantMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    // Subdomain'dan 2.2-bob: mana.app.uz  "mana"
    const subdomain = req.hostname.split(".")[0];
    // yoki JWT'dan: req.user.tenantId (8.9)
    req.tenantId = subdomain;
    if (!req.tenantId) throw new BadRequestException("Tenant aniqlanmadi");
    next();
  }
}

// AsyncLocalStorage (har so'rov uchun izolyatsiya — global kontekst)
// yoki REQUEST-scoped provider (8.2 scope)

Tenant kontekst — joriy so'rovning tenant'i (har so'rovda aniqlanadi, hamma joyda mavjud). Middleware/guard 8.6-bob tenant'ni aniqlaydi (subdomain/JWT — 2.2) kontekstga qo'yadi. NestJS: REQUEST-scoped provider (8.2 scope) yoki AsyncLocalStorage (so'rov izolyatsiyasi — har so'rov o'z kontekstida). Service'lar kontekstdan tenant'ni oladi (avtomatik filtr uchun — 2.5). Bu — tenant'ni butun so'rov bo'ylab tashish.

2.7. Row-Level Security (RLS — DB darajasida)

sql
-- PostgreSQL RLS — izolyatsiya DB'da (app emas — eng ishonchli)
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON customers
  USING (tenant_id = current_setting('app.current_tenant')::uuid);

-- Har so'rovdan oldin tenant o'rnatiladi:
-- SET app.current_tenant = 'mana-tenant-id';
--  endi customers'dan SELECT faqat shu tenant qatorlarini qaytaradi (DB kafolati)

Row-Level Security (RLS — eng ishonchli izolyatsiya): izolyatsiya DB darajasida (PostgreSQL — app kodiga bog'liq emas). POLICY — har qatorga tenant_id filtri avtomatik (DB qo'llaydi). Har so'rovdan oldin SET app.current_tenant (joriy tenant). Endi dasturchi filtr unutsa ham — DB faqat shu tenant qatorlarini beradi (kafolat). App filtri 2.5-bob + RLS 2.7-bob — ikki qatlam himoya. RLS — shared schema'ni xavfsiz qiladi.

2.8. Schema/Database per-tenant (kuchli izolyatsiya)

typescript
// Schema-per-tenant — dinamik ulanish (tenant'ga qarab schema)
async tenantConnection(tenantId: string): Promise<DataSource> {
  // Har tenant — alohida schema (yoki DB)
  return new DataSource({
    type: "postgres",
    schema: `tenant_${tenantId}`,                     // dinamik schema
    // database-per-tenant: database: `tenant_${tenantId}_db`
  }).initialize();
}
//  Connection pool boshqaruvi (har tenant ulanish — resurs — 6.15)
//  Migration — har schema/DB'ga (murakkab — 6.14)

Schema/Database-per-tenant (kuchli izolyatsiya): tenant'ga qarab dinamik ulanish (schema yoki DB). Yaxshiroq izolyatsiya (tenant_id filtri shart emas — alohida joy), lekin murakkab: connection pool (har tenant ulanish — resurs — 6.15), migration (har schema/DB'ga — 6.14 — qiyin). Enterprise/compliance (qattiq talab — bank, sog'liq) uchun. Ko'p tenant — qimmat (1000 tenant = 1000 schema/DB). Shared schema'dan boshlash afzal 2.3-bob.

2.9. Tenant onboarding (yangi tenant)

typescript
// Yangi tenant ro'yxatdan o'tkazish (kompaniya qo'shilish)
async tenantYarat(dto: CreateTenantDto): Promise<Tenant> {
  // 1. Tenant yozuvi
  const tenant = await this.tenantRepo.save({
    nom: dto.kompaniya, subdomain: dto.subdomain, reja: "trial",   // (8.29 subscription)
  });
  // 2. Admin user (kompaniya egasi)
  await this.usersService.yarat({ ...dto.admin, tenantId: tenant.id, rol: "owner" });
  // 3. Boshlang'ich ma'lumot (default sozlama, namuna)
  await this.seedDefaults(tenant.id);
  // 4. Schema-per-tenant bo'lsa: schema + migration (2.8)
  // 5. Xush kelibsiz email (8.10)
  return tenant;
}

Tenant onboarding (yangi kompaniya qo'shilishi — SaaS'ning muhim qismi): tenant yozuvi (subdomain, reja — 8.29) admin user (egasi) boshlang'ich ma'lumot (default sozlama) (schema-per-tenant — schema+migration) xush kelibsiz 8.10-bob. Self-service onboarding (kompaniya o'zi ro'yxatdan o'tadi — trial — 8.29) — SaaS o'sishining kaliti. Avtomatik, silliq onboarding — yaxshi SaaS belgisi.

2.10. Multi-tenancy xavfsizligi (14 — kritik)

text
   HAR so'rovda tenant filtri (avtomatik — qo'lga ishonmaslik — 2.5)
   RLS (DB darajasida himoya — 2.7) — ikkinchi qatlam
   Tenant ID JWT'da (o'zgartirib bo'lmaydigan — imzolangan — 8.9)
   Cross-tenant tekshirish (ID bo'yicha to'g'ridan kirish testi)
   Tenant kontekst har so'rovda majburiy (yo'q  rad)
   Resurs cheklash (bir tenant boshqasini sekinlatmasin — rate limit)
   Test: tenant A tenant B ma'lumotini KO'RA OLMASLIGI (8.11)
   Bir izolyatsiya xatosi = falokat (raqib ma'lumotini ko'rish)

Multi-tenancy xavfsizligi (eng kritik — 14): har so'rovda tenant filtri (avtomatik — 2.5) + RLS (2.7 — ikki qatlam); tenant ID JWT'da (imzolangan — o'zgartirib bo'lmaydi — 8.9; URL/header'da emas — soxtalashtiriladi); cross-tenant test (tenant A, B ma'lumotini ID bilan to'g'ridan so'rasa — rad bo'lishi — IDOR 8.7); resurs cheklash. Bir izolyatsiya xatosi = falokat (mijoz raqibining ma'lumotini ko'radi — biznes tugaydi, sud). Test majburiy (8.11 — izolyatsiya tekshiruvi).

2.11. Best practices (multi-tenancy)

text
   Strategiya tanlovi (shared schema + RLS — boshlash — 2.3)
   Tenant aniqlash (subdomain + JWT — 2.2)
   AVTOMATIK tenant filtri (qo'lga ishonmaslik — 2.5)
   RLS (DB himoya — ikkinchi qatlam — 2.7)
   Tenant kontekst (har so'rovda — request-scoped — 2.6)
   tenant_id indeks (tez filtr — 2.4); cross-tenant test (8.11, 2.10)
   Onboarding avtomatik (self-service trial — 2.9, 8.29)
   Resurs cheklash (tenant izolyatsiyasi — 2.10)
   Hybrid (katta tenant  alohida DB — 2.3)

2.12. Strategiya tanlovi — afzallik/kamchilik/qachon jadvali

Uch strategiyani 2.3-bob bir joyda solishtiramiz — bu jadval loyiha boshida qaror qabul qilishga yordam beradi. Har mezon bo'yicha (izolyatsiya, narx, masshtab, migration, sozlash) taqqoslash:

text
  ┌────────────────┬──────────────────┬──────────────────┬──────────────────┐
  │ Mezon          │ Shared schema    │ Schema-per-      │ Database-per-    │
  │                │ (+ tenant_id)    │ tenant           │ tenant           │
  ├────────────────┼──────────────────┼──────────────────┼──────────────────┤
  │ Izolyatsiya    │ Zaif (app/RLS)   │ O'rtacha         │ Maksimal         │
  │ Narx (infra)   │ Eng arzon        │ O'rtacha         │ Eng qimmat       │
  │ Masshtab       │ Oson (10k+)      │ Cheklangan       │ Qiyin            │
  │  (tenant soni) │                  │ (~yuzlab schema) │ (~o'nlab DB)     │
  │ Migration      │ Bir marta        │ Har schema'ga    │ Har DB'ga        │
  │ Connection pool│ Bitta pool       │ Ko'p ulanish     │ Ko'p pool        │
  │ Backup/restore │ Umumiy (tenant   │ Schema bo'yicha  │ Tenant bo'yicha  │
  │                │ ajratish qiyin)  │                  │ (oson, aniq)     │
  │ Xesh so'rov    │ Yaxshi (indeks)  │ Yaxshi           │ Yaxshi           │
  │ Qachon         │ MVP, ko'p kichik │ B2B, o'rta soni, │ Enterprise,      │
  │                │ tenant, tez o'sish│ o'rtacha izolyatsiya│ compliance (bank,│
  │                │                  │                  │ sog'liq), kam    │
  │                │                  │                  │ katta tenant     │
  └────────────────┴──────────────────┴──────────────────┴──────────────────┘

Qaror qoidasi: ko'p SaaS shared schema + tenant_id + RLS dan boshlaydi (arzon, oson masshtab, RLS bilan xavfsiz — 2.7). Regulyatsiya (bank, sog'liq — HIPAA/PCI) yoki mijoz fizik ajratishni talab qilsa — database-per-tenant. Schema-per-tenant — oraliq (o'rta soni tenant, kuchliroq izolyatsiya, lekin migration murakkabligi bilan). Hybrid amalda eng ko'p: kichik/o'rta tenant shared schema'da, katta (yoki compliance talab qiladigan) tenant alohida DB'ga ko'chiriladi. Boshidan database-per-tenant tanlash — ko'p SaaS uchun ortiqcha murakkablik (premature).

2.13. nestjs-cls va CLS bilan request-scope'siz kontekst

REQUEST-scoped provider 2.6-bob tenant kontekstini beradi, lekin kamchiligi: request-scope butun DI zanjirini so'rov-scope qiladi (har so'rovda provider qayta yaratiladi — performance narxi, singleton'lardan foydalanish murakkablashadi). Yaxshiroq yechim — AsyncLocalStorage (ALS) asosidagi kontekst: provider singleton qoladi, lekin kontekst har so'rov uchun izolyatsiyalangan (Node.js async zanjiri bo'ylab). nestjs-cls — shu ALS'ni NestJS uchun qulay o'raydi (CLS — Continuation-Local Storage).

typescript
// nestjs-cls — request-scope'siz tenant kontekst (singleton provider'lar bilan)
// app.module.ts
ClsModule.forRoot({
  middleware: { mount: true },           // har so'rovda yangi CLS kontekst
});

// Tenant'ni kontekstga qo'yish (middleware/guard ichida)
@Injectable()
export class TenantMiddleware implements NestMiddleware {
  constructor(private readonly cls: ClsService) {}
  use(req: any, _res: any, next: () => void) {
    const subdomain = req.hostname.split(".")[0];   // yoki JWT'dan (2.2)
    this.cls.set("tenantId", subdomain);            // kontekstga (so'rov izolyatsiyasi)
    next();
  }
}

// Service — SINGLETON, lekin kontekstdan tenant oladi (request-scope shart emas)
@Injectable()
export class CustomersService {
  constructor(private readonly cls: ClsService, private repo: Repository<Customer>) {}
  hammasi() {
    const tenantId = this.cls.get("tenantId");      // joriy so'rov tenant'i
    return this.repo.find({ where: { tenantId } });
  }
}

ALS/nestjs-cls afzalligi: provider'lar singleton qoladi (performance yaxshi — 8.2 scope), lekin tenant kontekst har so'rov uchun ajratilgan (ALS async zanjiri). Ehtiyot: ALS kontekst faqat async zanjir ichida saqlanadi — setTimeout/qo'lda EventEmitter callback'i zanjirdan chiqib ketishi mumkin (kontekst yo'qoladi). Odatiy request oqimida (controller service repo) muammo yo'q. Bu — Misol 1'dagi qo'lda AsyncLocalStorage yechimining tayyor, sinovdan o'tgan varianti.

2.14. Prisma bilan tenant izolyatsiyasi (extension/middleware)

TypeORM subscriber (Misol 3) o'rniga Prisma ishlatilsa, avtomatik tenant filtri client extension ($extends — yangi yo'l) yoki middleware ($use — eski, deprecated) orqali qo'yiladi. Extension har findMany/create so'roviga tenantId shartini avtomatik qo'shadi — dasturchi qo'lda yozmaydi.

typescript
// Prisma client extension — har so'rovga tenant filtri avtomatik
function tenantExtension(tenantId: string) {
  return Prisma.defineExtension((client) =>
    client.$extends({
      query: {
        $allModels: {
          async $allOperations({ args, query, operation }) {
            // O'qish/o'chirish/yangilash — where'ga tenantId qo'shiladi
            if (["findMany", "findFirst", "updateMany", "deleteMany", "count"].includes(operation)) {
              args.where = { ...args.where, tenantId };
            }
            // Yaratishda — tenantId majburiy o'rnatiladi
            if (operation === "create") {
              args.data = { ...args.data, tenantId };
            }
            return query(args);
          },
        },
      },
    }),
  );
}

// Har so'rovda tenant'ga bog'langan client (kontekstdan — 2.13)
@Injectable()
export class PrismaTenantService {
  constructor(private base: PrismaService, private cls: ClsService) {}
  get client() {
    return this.base.$extends(tenantExtension(this.cls.get("tenantId")) as any);
  }
}

Prisma yo'li: $extends (client extension) — avtomatik tenantId filtri (o'qish/yozishda). $use middleware (eski API) deprecated — yangi loyihada $extends ishlating. Diqqat: create da tenantId'ni majburan o'rnating (mijoz yuborgan tenantId'ga ishonmang — soxtalashtirilishi mumkin). Cross-tenant leak oldini olish uchun findUnique'da ham (ID bo'yicha) natijaning tenantId'sini tekshiring — Prisma findUnique where'ga qo'shimcha shart qo'shishga ba'zan cheklov qo'yadi (u holda findFirst ishlating). RLS 2.7-bob bilan birga ishlatish — ikki qatlam himoya.

2.15. Migratsiya barcha tenantlar bo'yicha

Shared schema'da migration bir marta ishlaydi (bitta schema — barcha tenant'ga ta'sir). Lekin schema-per-tenant yoki database-per-tenant da migration har tenant uchun alohida ishlatilishi kerak — bu multi-tenancy'ning eng murakkab operatsion qismi 6.14-bob.

typescript
// Schema-per-tenant — barcha tenantga migration (loop bilan)
@Injectable()
export class TenantMigrationService {
  constructor(private tenantRepo: TenantRepository, private config: ConfigService) {}

  async migrateAll() {
    const tenants = await this.tenantRepo.find({ where: { faol: true } });
    for (const tenant of tenants) {
      const ds = new DataSource({
        type: "postgres",
        schema: `tenant_${tenant.id}`,                // shu tenant schema'si
        migrations: ["dist/migrations/*.js"],
        entities: [Customer, Product],
      });
      await ds.initialize();
      try {
        await ds.runMigrations();                     // shu schema'ga qo'llaydi
      } finally {
        await ds.destroy();                           // ulanishni yopish (resurs — 6.15)
      }
    }
  }
}
//  Yangi tenant onboarding'da 2.9-bob darhol migration ishga tushiriladi (schema tayyor bo'lsin)

Per-tenant migration — kritik operatsion masala. Bir tenant'da migration muvaffaqiyatsiz bo'lsa (masalan ulanish uzildi) — qolganlaridan oldinga o'tmang ko'r-ko'rona: qaysi tenant migratsiya bo'lgani/bo'lmaganini kuzating (holat jadvali). Katta tenant sonida — navbat (queue — 8.22) yoki partiyalab (batch) bajaring (bir vaqtda hammasi — DB'ni bo'g'adi). Yangi tenant onboarding'ida 2.9-bob schema yaratilgach darhol barcha migration'lar qo'llanadi. Deploy paytida: migration barcha tenant'ga tarqalguncha ilova ikki versiyaga mos bo'lishi kerak (backward-compatible migration — 6.14).

2.16. Tenant-specific config, feature-flag va branding

Har tenant o'z sozlamasi bilan ishlaydi: valyuta, til, mavzu (rang/logo — branding), va feature-flag (rejaga qarab funksiya yoqilgan/o'chirilgan — 8.29 subscription bilan bog'liq). Bu sozlamalar TenantSettings jadvalida (shared schema) yoki tenant DB'da saqlanadi va kontekst orqali o'qiladi.

typescript
@Entity()
export class TenantSettings {
  @PrimaryColumn() tenantId: string;
  @Column({ default: "UZS" }) valyuta: string;
  @Column({ default: "uz" }) til: string;
  @Column({ type: "jsonb", default: {} }) branding: {           // logo, rang (UI)
    logoUrl?: string; asosiyRang?: string;
  };
  @Column({ type: "jsonb", default: {} }) features: Record<string, boolean>;   // feature-flag
}

// Feature-flag tekshiruvi (reja bilan bog'liq — 8.29)
@Injectable()
export class FeatureService {
  constructor(private repo: Repository<TenantSettings>, private cls: ClsService) {}
  async yoqilganmi(feature: string): Promise<boolean> {
    const s = await this.repo.findOneBy({ tenantId: this.cls.get("tenantId") });
    return s?.features?.[feature] ?? false;                      // default — o'chiq
  }
}

// Guard: funksiya rejaga kirmasa — rad (masalan "advancedReports" faqat pro rejada)
@Injectable()
export class FeatureGuard implements CanActivate {
  constructor(private reflector: Reflector, private features: FeatureService) {}
  async canActivate(ctx: ExecutionContext): Promise<boolean> {
    const need = this.reflector.get<string>("feature", ctx.getHandler());
    if (need && !(await this.features.yoqilganmi(need))) {
      throw new ForbiddenException("Bu funksiya rejangizga kirmaydi");   // upsell (8.29)
    }
    return true;
  }
}

Tenant config/feature-flag/branding: har tenant o'z sozlamasi (valyuta, til, logo/rang) bilan — TenantSettings (kontekstdan o'qiladi). Feature-flag — rejaga qarab funksiya yoqiladi (8.29 subscription bilan chambarchas): pro reja "kengaytirilgan hisobot"ni ochadi, trial ochmaydi. Feature rad etilganda — foydalanuvchiga upgrade taklifi (upsell — biznes qiymat). Branding tenant'ga "o'z ilovasi" hissini beradi (white-label — enterprise SaaS'da muhim).

2.17. Per-tenant rate-limit va kvota (resurs adolati)

Multi-tenant tizimda bir tenant (yoki uning bir bag'li skripti) haddan ziyod so'rov yuborsa — boshqa tenant'larni sekinlashtiradi ("shovqinli qo'shni" — noisy neighbor muammosi). Yechim: rate-limit va kvotani tenant bo'yicha qo'llash (global emas), odatda rejaga bog'lab (8.29 — pro tenant yuqori limit).

typescript
// Tenant bo'yicha rate-limit — throttler kalitini tenant'dan olish
@Injectable()
export class TenantThrottlerGuard extends ThrottlerGuard {
  // Limitni tenant bo'yicha izolyatsiya qilish (global emas)
  protected async getTracker(req: Record<string, any>): Promise<string> {
    return req.tenantId ?? req.ip;                    // tenant'ga bog'langan hisoblagich
  }
}

// Kvota — oylik so'rov/yozuv soni (rejaga qarab — 8.29)
@Injectable()
export class QuotaService {
  constructor(private redis: Redis, private cls: ClsService) {}
  async iste'molOshir(metric: string, limit: number) {
    const tenantId = this.cls.get("tenantId");
    const oy = new Date().toISOString().slice(0, 7);              // "2026-07"
    const key = `quota:${tenantId}:${metric}:${oy}`;
    const joriy = await this.redis.incr(key);
    if (joriy === 1) await this.redis.expire(key, 32 * 24 * 3600);  // oy oxirida tozalanadi
    if (joriy > limit) {
      throw new ForbiddenException("Oylik kvota tugadi — rejani oshiring");   // 8.29-bob
    }
  }
}

Per-tenant rate-limit/kvota ("noisy neighbor" oldini olish): rate-limit tenant kaliti bo'yicha (bir tenant boshqasini bo'g'masin — resurs adolati). Kvota — oylik iste'mol (so'rov, yozuv, storage) rejaga bog'lab (8.29 — pro yuqori limit). Kvota tugaganda — rad + upgrade taklifi (biznes qiymat). Hisoblagichni Redis'da tenant+metric+oy kaliti bilan saqlang (atomik incr). Bu ham izolyatsiyaning bir turi — resurs izolyatsiyasi (ma'lumot izolyatsiyasi — 2.10 — bilan birga).

2.18. Tenant o'chirish — data export va GDPR

Tenant obunani bekor qilsa yoki o'chirilsa — ma'lumotni darhol o'chirmang: (1) avval export taklif eting (mijoz o'z ma'lumotini olsin — GDPR "portability" huquqi), (2) grace period (masalan 30 kun — fikridan qaytishi mumkin), (3) keyin butunlay o'chirish (GDPR "right to erasure" / "unutilish huquqi"). Yumshoq o'chirish (soft delete) bilan boshlang, so'ng qattiq o'chirish.

typescript
@Injectable()
export class TenantLifecycleService {
  constructor(private dataSource: DataSource, private storage: StorageService) {}

  // 1. Data export — tenant'ning barcha ma'lumotini fayl sifatida (GDPR portability)
  async export(tenantId: string): Promise<string> {
    const data = {
      customers: await this.dataSource.getRepository(Customer).findBy({ tenantId }),
      products: await this.dataSource.getRepository(Product).findBy({ tenantId }),
      // ... barcha tenant jadvallari
    };
    return this.storage.saqla(`export/${tenantId}.json`, JSON.stringify(data));  // yuklab olish havolasi
  }

  // 2. Yumshoq o'chirish — grace period (30 kun, fikridan qaytishi mumkin)
  async deaktivatsiya(tenantId: string) {
    await this.dataSource.getRepository(Tenant).update(tenantId, {
      faol: false,
      ochirishSanasi: new Date(Date.now() + 30 * 24 * 3600 * 1000),   // 30 kun keyin
    });
  }

  // 3. Qattiq o'chirish — grace period tugagach (GDPR right to erasure)
  async butunlayOchir(tenantId: string) {
    await this.dataSource.transaction(async (manager) => {
      // Bog'liqlik tartibida o'chirish (yoki cascade — 8.3)
      await manager.delete(Customer, { tenantId });
      await manager.delete(Product, { tenantId });
      await manager.delete(TenantSettings, { tenantId });
      await manager.delete(User, { tenantId });
      await manager.delete(Tenant, { id: tenantId });
      // Schema/db-per-tenant bo'lsa: DROP SCHEMA tenant_<id> CASCADE (2.8)
    });
    await this.storage.ochir(`export/${tenantId}.json`);   // export faylini ham (grace tugadi)
  }
}

Tenant o'chirish (data export + GDPR): darhol o'chirmang — (1) export (mijoz ma'lumotini oladi — GDPR portability), (2) grace period (30 kun — soft delete, faol=false), (3) qattiq o'chirish (GDPR erasure — unutilish huquqi). Schema/db-per-tenant'da o'chirish oson va aniq (DROP SCHEMA — 2.12 jadvalidagi afzallik). Shared schema'da — har jadvaldan tenantId bo'yicha o'chirish (cascade — 8.3). Backup'lardagi ma'lumot ham GDPR muddatida tozalanishi kerak (retention siyosati). Bu — SaaS'ning huquqiy majburiyati (jarima — GDPR bo'yicha yillik aylanmaning 4% gacha).

2.19. Billing bilan bog'liqlik va performance/scaling

Multi-tenancy billing (8.29 — subscription/obuna) bilan chambarchas: har tenant o'z rejasi (trial/basic/pro), reja feature-flag 2.16-bob va kvota 2.17-bob ni belgilaydi; iste'mol (usage) o'lchanadi va hisob-kitobga aylanadi (usage-based billing). Tenant reja o'zgarishi (upgrade/downgrade) — limitlar darhol yangilanadi.

typescript
// Reja  limit/feature xaritasi (8.29 subscription bilan)
const REJALAR = {
  trial: { maxUsers: 3, apiLimit: 1000, features: ["basic"] },
  basic: { maxUsers: 10, apiLimit: 50_000, features: ["basic"] },
  pro:   { maxUsers: 100, apiLimit: 500_000, features: ["basic", "advancedReports", "api"] },
};

// Tenant iste'molini billing uchun o'lchash (usage-based — 8.29)
@Injectable()
export class UsageService {
  async yozib(tenantId: string, metric: "apiCall" | "storage", miqdor = 1) {
    // Oylik iste'mol — hisob davri oxirida billing'ga uzatiladi (8.29)
    // (2.17 kvota bilan bir manba — bir marta o'lchang, ikkala maqsadga ishlating)
  }
}

Performance va scaling bo'yicha asosiy nuqtalar:

text
   tenant_id — HAR filtrlanadigan jadvalda indeks 2.4-bob; ko'p ustunli
    so'rovda composite indeks (tenant_id, boshqa_ustun) — 6.10
   "Katta tenant" (data hajmi katta) — shared schema'da boshqa kichiklarni
    sekinlatishi mumkin  hybrid: alohida DB'ga ko'chirish 2.12-bob
   Connection pool — shared schema'da bitta pool (samarali); schema/db-per-
    tenant'da ulanish soni tez oshadi  cache + limit (Misol 9, 6.15)
   Cache kaliti — HAR doim tenant_id bilan (cross-tenant cache leak
    oldini olish — noto'g'ri tenant'ga cache berilmasin) — 6.16
   Fon vazifalari (queue — 8.22) — job payload'ida tenantId; worker
    kontekstni tiklaydi (ALS kontekst HTTP so'rovsiz)
   Monitoring — metrikalarni tenant bo'yicha teglash (qaysi tenant og'ir)

Billing bog'liqligi: reja 8.29-bob feature-flag 2.16-bob + kvota 2.17-bob; iste'mol o'lchanadi (usage-based billing). Kvota va usage — bir manba (bir marta o'lchang, ikkalasiga ishlating). Performance/scaling: tenant_id indeks (composite — 6.10); cache kaliti har doim tenant_id bilan (cross-tenant cache leak — nozik, xavfli xato); fon vazifasida 8.22-bob tenantId'ni payload'da uzating (HTTP so'rov yo'q — kontekst avtomatik tiklanmaydi); katta tenant'ni hybrid'da alohida DB'ga 2.12-bob. Monitoringda metrikalarni tenant bo'yicha teglang — qaysi tenant tizimni og'irlashtirayotganini ko'rish uchun.


3. Sintaksis — tez ma'lumotnoma

text
TENANT ANIQLASH 2.2-bob: subdomain | JWT tenantId | X-Tenant-ID header
STRATEGIYA 2.3-bob: shared+tenant_id | schema-per-tenant | db-per-tenant

Shared schema 2.4-bob: har jadval @Column tenantId @Index
Avtomatik filtr 2.5-bob: repository wrapper / TypeORM subscriber / RLS
RLS 2.7-bob: CREATE POLICY ... USING (tenant_id = current_setting('app.current_tenant'))
Kontekst (2.6, 2.13): middleware  cls.set("tenantId"); REQUEST-scoped / AsyncLocalStorage / nestjs-cls
Prisma filtr 2.14-bob: client.$extends({ query: { $allModels: {...} } })
Feature-flag 2.16-bob: FeatureGuard + @SetMetadata("feature", "pro-funksiya")
Kvota 2.17-bob: tenant bo'yicha throttler getTracker()  tenantId; Redis incr

4. Batafsil kod namunalari

Misol 1 — Tenant entity va kontekst (2.1, 2.6)

typescript
@Entity()
export class Tenant {
  @PrimaryGeneratedColumn("uuid") id: string;
  @Column({ unique: true }) subdomain: string;        // mana.app.uz
  @Column() nom: string;
  @Column({ default: "trial" }) reja: string;         // subscription (8.29)
  @Column({ default: true }) faol: boolean;
}

// Tenant kontekst (AsyncLocalStorage — so'rov izolyatsiyasi)
import { AsyncLocalStorage } from "async_hooks";

@Injectable()
export class TenantContext {
  private als = new AsyncLocalStorage<{ tenantId: string }>();

  run(tenantId: string, callback: () => any) {
    return this.als.run({ tenantId }, callback);
  }
  get tenantId(): string {
    const store = this.als.getStore();
    if (!store) throw new Error("Tenant kontekst yo'q");
    return store.tenantId;
  }
}

Misol 2 — Tenant middleware (aniqlash — 2.2, 2.6)

typescript
@Injectable()
export class TenantMiddleware implements NestMiddleware {
  constructor(private tenantRepo: TenantRepository, private context: TenantContext) {}

  async use(req: Request, res: Response, next: () => void) {
    // 1. Subdomain'dan (2.2)
    const host = req.hostname;                         // mana.app.uz
    const subdomain = host.split(".")[0];

    // 2. Tenant topish
    const tenant = await this.tenantRepo.findBySubdomain(subdomain);
    if (!tenant || !tenant.faol) {
      throw new NotFoundException("Tenant topilmadi yoki faol emas");
    }

    // 3. Kontekstga qo'yish (butun so'rov bo'ylab)
    this.context.run(tenant.id, () => next());
  }
}
// app.module: consumer.apply(TenantMiddleware).forRoutes("*")

Misol 3 — Avtomatik tenant filtri (TypeORM subscriber — 2.5)

typescript
// TypeORM subscriber — har save/query'ga tenant_id avtomatik
@EventSubscriber()
export class TenantSubscriber implements EntitySubscriberInterface {
  constructor(dataSource: DataSource, private context: TenantContext) {
    dataSource.subscribers.push(this);
  }

  beforeInsert(event: InsertEvent<any>) {
    if ("tenantId" in event.entity) {
      event.entity.tenantId = this.context.tenantId;  // AVTOMATIK qo'shiladi (2.5)
    }
  }
}

// Query filtri — global (yoki tenant-aware repository)
@Injectable()
export class CustomersService {
  constructor(
    @InjectRepository(Customer) private repo: Repository<Customer>,
    private context: TenantContext,
  ) {}

  hammasi() {
    return this.repo.find({ where: { tenantId: this.context.tenantId } });   // avtomatik (2.5)
  }
  async bitta(id: string) {
    const c = await this.repo.findOne({ where: { id, tenantId: this.context.tenantId } });
    if (!c) throw new NotFoundException();             // boshqa tenant  topilmadi (izolyatsiya)
    return c;
  }
}

Misol 4 — RLS sozlash (PostgreSQL — 2.7)

typescript
// RLS — har so'rovdan oldin tenant o'rnatish (DB himoyasi)
@Injectable()
export class RlsService {
  constructor(@InjectDataSource() private dataSource: DataSource, private context: TenantContext) {}

  // Har so'rovda tenant'ni DB sessiyasiga o'rnatish
  async withTenant<T>(callback: (manager: EntityManager) => Promise<T>): Promise<T> {
    return this.dataSource.transaction(async (manager) => {
      await manager.query(`SET LOCAL app.current_tenant = '${this.context.tenantId}'`);
      return callback(manager);                        // endi barcha so'rov RLS bilan
    });
  }
}

// migration — RLS policy (6.14)
// ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
// CREATE POLICY tenant_iso ON customers USING (tenant_id = current_setting('app.current_tenant')::uuid);

Misol 5 — Tenant guard (JWT'dan — 2.2, 2.10)

typescript
// JWT'da tenantId (login'da belgilanadi — 8.9, imzolangan — o'zgartirib bo'lmaydi)
@Injectable()
export class TenantGuard implements CanActivate {
  constructor(private context: TenantContext) {}

  canActivate(ctx: ExecutionContext): boolean {
    const req = ctx.switchToHttp().getRequest();
    const tenantId = req.user?.tenantId;              // JWT payload (8.9)
    if (!tenantId) throw new ForbiddenException("Tenant aniqlanmadi");
    req.tenantId = tenantId;
    return true;
  }
}

// JWT payload (8.9 — login'da tenantId qo'shiladi)
// { sub: userId, tenantId: "mana-id", rol: "admin" }

Misol 6 — Tenant onboarding (2.9)

typescript
@Injectable()
export class OnboardingService {
  async tenantYarat(dto: CreateTenantDto): Promise<Tenant> {
    // Subdomain band emasligini tekshir
    if (await this.tenantRepo.findBySubdomain(dto.subdomain)) {
      throw new ConflictException("Bu subdomain band");
    }
    return this.dataSource.transaction(async (manager) => {   // atomik (8.13)
      // 1. Tenant
      const tenant = await manager.save(Tenant, {
        subdomain: dto.subdomain, nom: dto.kompaniya, reja: "trial",
        trialTugashi: new Date(Date.now() + 14 * 24 * 3600 * 1000),   // 14 kun (8.29)
      });
      // 2. Admin user
      await manager.save(User, {
        tenantId: tenant.id, email: dto.adminEmail,
        parol: await bcrypt.hash(dto.parol, 12), rol: "owner",   // (8.7, 8.9)
      });
      // 3. Default sozlama
      await manager.save(TenantSettings, { tenantId: tenant.id, valyuta: "UZS" });
      return tenant;
    });
    // 4. Xush kelibsiz email 8.10-bob — transaction'dan keyin (fonda — 8.22)
  }
}

Misol 7 — Tenant-aware controller (2.6)

typescript
@Controller("customers")
@UseGuards(JwtAuthGuard, TenantGuard)                 // auth + tenant (8.9, 2.5)
export class CustomersController {
  constructor(private service: CustomersService) {}

  @Get()
  hammasi() {
    return this.service.hammasi();                    // avtomatik tenant filtri (Misol 3)
  }

  @Get(":id")
  bitta(@Param("id") id: string) {
    return this.service.bitta(id);                    // boshqa tenant  404 (izolyatsiya)
  }

  @Post()
  yarat(@Body() dto: CreateCustomerDto) {
    return this.service.yarat(dto);                   // tenant_id avtomatik (subscriber — Misol 3)
  }
}
// Dasturchi tenant_id'ni o'ylamaydi (avtomatik — xato kamayadi)

Misol 8 — Cross-tenant izolyatsiya testi (2.10, 8.11)

typescript
// ENG MUHIM test — izolyatsiya buzilmasligini isbotlash
describe("Tenant izolyatsiyasi (e2e)", () => {
  it("tenant A tenant B mijozini KO'RA OLMAYDI", async () => {
    // Tenant A mijoz yaratadi
    const tokenA = await login("admin@tenant-a.uz");
    const mijozA = await request(app.getHttpServer())
      .post("/customers").set("Authorization", `Bearer ${tokenA}`)
      .send({ ism: "Mijoz A" }).expect(201);

    // Tenant B uning ID'si bilan to'g'ridan so'raydi (IDOR urinishi — 8.7)
    const tokenB = await login("admin@tenant-b.uz");
    await request(app.getHttpServer())
      .get(`/customers/${mijozA.body.id}`)            // A'ning mijozi!
      .set("Authorization", `Bearer ${tokenB}`)
      .expect(404);                                   // KO'RA OLMAYDI (izolyatsiya — to'g'ri!)
  });
});
//  Bu test O'TMASA — izolyatsiya buzilgan (falokat)

Misol 9 — Schema-per-tenant (kuchli izolyatsiya — 2.8)

typescript
// Har tenant — alohida schema (dinamik ulanish)
@Injectable()
export class TenantConnectionService {
  private connections = new Map<string, DataSource>();   // cache (connection pool — 6.15)

  async getConnection(tenantId: string): Promise<DataSource> {
    if (this.connections.has(tenantId)) return this.connections.get(tenantId)!;

    const ds = new DataSource({
      type: "postgres",
      host: this.config.get("DB_HOST"),
      schema: `tenant_${tenantId}`,                   // alohida schema (2.8)
      entities: [Customer, Product],
    });
    await ds.initialize();
    this.connections.set(tenantId, ds);
    return ds;
  }
  //  Migration har schema'ga 6.14-bob; connection limit (6.15)
}

Misol 10 — To'liq multi-tenant arxitektura

text
  SaaS multi-tenant tuzilma:
  src/
  ├── tenancy/
  │   ├── tenant.context.ts          (AsyncLocalStorage — Misol 1)
  │   ├── tenant.middleware.ts       (aniqlash — Misol 2)
  │   ├── tenant.guard.ts            (JWT — Misol 5)
  │   ├── tenant.subscriber.ts       (avtomatik filtr — Misol 3)
  │   └── onboarding.service.ts      (yangi tenant — Misol 6)
  ├── customers/ (tenant-aware — Misol 7)
  └── ...

  Har so'rov oqimi:
  Request (mana.app.uz)  TenantMiddleware (subdomain  tenant)
   JWT auth (tenantId — 8.9)  TenantContext (kontekst)
   Service (avtomatik tenant filtri — Misol 3) + RLS (DB himoya — Misol 4)
   faqat shu tenant ma'lumoti (izolyatsiya — 2.10)

   Strategiya: shared schema + RLS (boshlash)  katta tenant alohida DB (hybrid)

5. To'g'ri va noto'g'ri holatlar

1) Qo'lda tenant filtri (har so'rovda)

text
 where: { tenantId } qo'lda (unutiladi  oqish — 2.5)
 avtomatik (subscriber/RLS)

2) Tenant ID URL/header'da (JWT emas)

text
 ?tenantId=... (soxtalashtiriladi — 2.10)
 JWT'da (imzolangan — o'zgartirib bo'lmaydi)

3) Izolyatsiya faqat app'da (RLS yo'q)

text
 bir kod xatosi  oqish (2.7)
 app filtri + RLS (ikki qatlam)

4) Cross-tenant test yo'q

text
 izolyatsiya tekshirilmagan (2.10)
 AB izolyatsiya testi (Misol 8)

5) Kichik SaaS'ga database-per-tenant

text
 1000 kichik tenant  1000 DB (qimmat — 2.3)
 shared schema + RLS

6. Keng tarqalgan xatolar va yechimlari

Xato 1 — Ma'lumot oqishi (cross-tenant)

Sababi: tenant filtri unutilgan 2.5-bob. Yechimi: avtomatik filtr + RLS + test.

Xato 2 — tenant_id sekin filtr

Sababi: indeks yo'q 2.4-bob. Yechimi: tenantId indeks (composite — 6.10).

Xato 3 — Tenant kontekst yo'q

Sababi: middleware ishlamagan 2.6-bob. Yechimi: middleware forRoutes("*").

Xato 4 — Schema-per-tenant connection limit

Sababi: har tenant ulanish 6.15-bob. Yechimi: connection cache; pool limit.

Xato 5 — Migration faqat bir schema'da

Sababi: per-tenant migration unutilgan 2.8-bob. Yechimi: har schema'ga migration 6.14-bob.

Xato 6 — Tenant ID soxtalashtirilgan

Sababi: header/URL'dan 2.10-bob. Yechimi: JWT (imzolangan).


7. Integratsiya — bu mavzu stack'ning qayerida uchraydi

  • DB 8.3-bob: tenant_id, RLS.
  • Auth 8.9-bob: JWT tenantId.
  • Guard/Middleware 8.6-bob: tenant aniqlash.
  • Subscription 8.29-bob: tenant reja/obuna.
  • Migration 6.14-bob: per-tenant.
  • Connection pool 6.15-bob: schema/db-per-tenant.
  • Test 8.11-bob: izolyatsiya testi.
  • Xavfsizlik (14): izolyatsiya, IDOR 8.7-bob.
  • Rate-limit (throttler): per-tenant kvota 2.17-bob.
  • Cache 6.16-bob: kalit tenant_id bilan (leak oldini olish — 2.19).
  • Queue 8.22-bob: job payload'ida tenantId 2.19-bob.
  • GDPR/huquqiy: data export, erasure (tenant o'chirish — 2.18).

8. Eng yaxshi amaliyotlar (best practices)

  • Strategiya (shared schema + RLS — boshlash — 2.3).
  • Tenant aniqlash (subdomain + JWT — 2.2).
  • Avtomatik tenant filtri (qo'lga ishonmaslik — 2.5).
  • RLS (DB himoya — ikkinchi qatlam — 2.7).
  • Tenant kontekst (har so'rovda — 2.6).
  • tenant_id indeks (tez — 2.4); cross-tenant test 2.10-bob.
  • Onboarding avtomatik (self-service trial — 2.9).
  • Tenant ID JWT'da (imzolangan — 2.10).
  • Resurs cheklash (per-tenant rate-limit/kvota — 2.17).
  • Hybrid (katta tenant alohida DB — 2.12).
  • Cache/queue kaliti tenant_id bilan (leak oldini olish — 2.19).
  • Tenant o'chirishda export + GDPR (grace period — 2.18).

9. Amaliy loyiha: "Multi-tenant SaaS CRM"

Multi-tenancy'ni amalda mustahkamlash.

Maqsad

Multi-tenant SaaS CRM: tenant aniqlash, izolyatsiya (shared schema + RLS), onboarding, test.

Talablar (requirements)

  1. Tenant entity + kontekst: AsyncLocalStorage (Misol 1, 2.6).
  2. Middleware: subdomain aniqlash (Misol 2, 2.2).
  3. Guard: JWT tenantId (Misol 5, 2.10).
  4. Avtomatik filtr: subscriber + service (Misol 3, 2.5).
  5. RLS: PostgreSQL policy (Misol 4, 2.7).
  6. Onboarding: yangi tenant (atomik — Misol 6, 2.9).
  7. Tenant-aware: controller/service (Misol 7).
  8. Izolyatsiya testi: cross-tenant (Misol 8, 2.10).
  9. Indeks: tenant_id 2.4-bob.
  10. Strategiya: shared schema + RLS 2.3-bob.

Maslahatlar (hint)

  • Avtomatik filtr (qo'lga ishonmaslik — 2.5, 1-xato).
  • RLS ikkinchi qatlam (2.7, 3-holat).
  • JWT tenantId (2.10, 2-holat).
  • tenant_id indeks (2.4, 2-xato).
  • Cross-tenant test (Misol 8, 4-holat).

"Tayyor" mezonlari (acceptance criteria)

  • Tenant entity + kontekst.
  • Middleware (subdomain).
  • Guard (JWT).
  • Avtomatik filtr.
  • RLS.
  • Onboarding.
  • Tenant-aware.
  • Izolyatsiya testi (O'TADI).
  • Indeks.
  • Strategiya.

Yechim kodi ataylab berilmagan — bu loyihani o'zingiz yozib ko'ring.


10. Xulosa va keyingi bobga ko'prik

Bu bobda multi-tenancy (SaaS)ni o'rgandik:

  • SaaS/tenant 2.1-bob; tenant aniqlash (subdomain/JWT — 2.2); 3 strategiya (shared/schema/db — 2.3).
  • Shared schema + tenant_id 2.4-bob; avtomatik filtri 2.5-bob; tenant kontekst 2.6-bob; RLS (DB himoya — 2.7).
  • Schema/db-per-tenant 2.8-bob; onboarding 2.9-bob; xavfsizlik (izolyatsiya — kritik — 2.10).

Keyingi bob — 8.26: Audit log va faoliyat tarixi. Multi-tenancy'ni bildik; endi yana bir real mavzu — audit log (kim, nima, qachon qildi — o'zgarishlar tarixi) — ni o'rganamiz. Xavfsizlik, hisobdorlik, compliance uchun zarur (ayniqsa SaaS/moliyaviy tizimda).


Foydalanilgan rasmiy/ishonchli manbalar

  • NestJS rasmiy hujjati — Execution context, Middleware, Guards, Custom providers (request-scoped)
  • PostgreSQL rasmiy hujjati — Row Security Policies (CREATE POLICY, ALTER TABLE ... ENABLE ROW LEVEL SECURITY)
  • TypeORM rasmiy hujjati — DataSource, Subscribers, Multiple data sources
  • Prisma rasmiy hujjati — Client extensions ($extends) va middleware; multi-tenancy yondashuvlari
  • nestjs-cls — CLS/AsyncLocalStorage asosidagi request kontekst kutubxonasi rasmiy hujjati
  • Node.js rasmiy hujjati — AsyncLocalStorage (async_hooks)

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
8.25-bob: Multi-tenancy (SaaS) — Wisar