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
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)
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:
// 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.uzDB/cache'dan domentenant xaritasi — white-label mijoz o'z domenidan kiradi), header (X-Tenant-ID— faqat API/server-server), JWT (token ichidatenantId— login'da — 8.9), path (app.uz/mana/...— kamroq, chalkash). Ustuvorlik tartibi kritik: JWT (imzolangan — ishonchli) URL/header (soxtalashtiriladigan) dan ustun — token bilan kirgan foydalanuvchiX-Tenant-IDheader'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)
┌──────────────────────┬────────────────────────────────────────┐
│ 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_idustun, 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)
// 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
tenantIdustun (qaysi tenant — indeks bilan — tez filtr). Har so'rovdatenantIdfiltri (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)
// 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
tenantIdfiltri 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)
// 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)
-- 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 qatorgatenant_idfiltri avtomatik (DB qo'llaydi). Har so'rovdan oldinSET 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)
// 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)
// 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)
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)
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:
┌────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ 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).
// 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'ldaEventEmittercallback'i zanjirdan chiqib ketishi mumkin (kontekst yo'qoladi). Odatiy request oqimida (controller service repo) muammo yo'q. Bu — Misol 1'dagi qo'ldaAsyncLocalStorageyechimining 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.
// 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) — avtomatiktenantIdfiltri (o'qish/yozishda).$usemiddleware (eski API)deprecated— yangi loyihada$extendsishlating. Diqqat:createdatenantId'ni majburan o'rnating (mijoz yuborgantenantId'ga ishonmang — soxtalashtirilishi mumkin). Cross-tenant leak oldini olish uchunfindUnique'da ham (ID bo'yicha) natijaningtenantId'sini tekshiring — PrismafindUniquewhere'ga qo'shimcha shart qo'shishga ba'zan cheklov qo'yadi (u holdafindFirstishlating). 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.
// 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.
@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):proreja "kengaytirilgan hisobot"ni ochadi,trialochmaydi. 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).
// 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.
@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 jadvaldantenantIdbo'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.
// 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:
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_idindeks (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
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 incr4. Batafsil kod namunalari
Misol 1 — Tenant entity va kontekst (2.1, 2.6)
@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)
@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)
// 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)
// 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)
// 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)
@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)
@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)
// 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)
// 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
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)
where: { tenantId } qo'lda (unutiladi oqish — 2.5)
avtomatik (subscriber/RLS)2) Tenant ID URL/header'da (JWT emas)
?tenantId=... (soxtalashtiriladi — 2.10)
JWT'da (imzolangan — o'zgartirib bo'lmaydi)3) Izolyatsiya faqat app'da (RLS yo'q)
bir kod xatosi oqish (2.7)
app filtri + RLS (ikki qatlam)4) Cross-tenant test yo'q
izolyatsiya tekshirilmagan (2.10)
AB izolyatsiya testi (Misol 8)5) Kichik SaaS'ga database-per-tenant
1000 kichik tenant 1000 DB (qimmat — 2.3)
shared schema + RLS6. 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)
- Tenant entity + kontekst: AsyncLocalStorage (Misol 1, 2.6).
- Middleware: subdomain aniqlash (Misol 2, 2.2).
- Guard: JWT tenantId (Misol 5, 2.10).
- Avtomatik filtr: subscriber + service (Misol 3, 2.5).
- RLS: PostgreSQL policy (Misol 4, 2.7).
- Onboarding: yangi tenant (atomik — Misol 6, 2.9).
- Tenant-aware: controller/service (Misol 7).
- Izolyatsiya testi: cross-tenant (Misol 8, 2.10).
- Indeks: tenant_id 2.4-bob.
- 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!