Kontentga o'tish
Tezbyte
Qo'ng'iroq band qilish
← Barcha maqolalar

NestJS + Prisma asosida multi-tenant SaaS arxitekturasi

Har qanday SaaS 2-mijoz ro'yxatdan o'tgan kuni multi-tenant'ga aylanadi. O'sha hafta qabul qiladigan izolatsiya qarori sizni yillar davomida kuzatib yuradi, shuning uchun mana biz aslida qanday qaror qabul qilamiz — production'da NestJS + Prisma + Postgres tizimlarini ishga tushirishda duch kelgan trade-off'lar bilan birga.

Ikkita haqiqiy variant

Row-level izolatsiya: bitta sxema, hamma joyda tenantId

Har bir tenant'ga tegishli jadval tenantId ustunini olib yuradi; har bir so'rov shu ustun bo'yicha filtrlanadi. Bitta baza, bitta migratsiya, bitta connection pool.

Yutuqlari: operatsion jihatdan zerikarli darajada oddiy. Migratsiyalar bir marta ishga tushadi. Tenant'lararo analitika — ETL job emas, oddiy so'rov. Connection pooling shunchaki ishlayveradi — bu odamlar o'ylagandan ko'ra muhimroq, chunki Postgres ulanishlari cheklangan resurs, va tenant-boshiga har qanday narsa ularni ko'paytiradi.

Xarajatlari: izolatsiya kodingizning hamma joyda, doimo to'g'ri bo'lishi orqali ta'minlanadi. Bitta unutilgan where: { tenantId } — va tenant A tenant B'ning invoyslarini ko'radi. Bu bug emas, bu churn hodisasi, va ehtimol huquqiy hodisa ham.

Schema-per-tenant: har bir mijoz uchun alohida Postgres sxemasi

Yutuqlari: yetishmayotgan filtr ma'lumot sizib chiqishi o'rniga baland ovozda muvaffaqiyatsizlikka uchraydi. "Sizning ma'lumotlaringiz fizik jihatdan ajratilgan" — korporativ xaridorlarga yoqadigan gap. Tenant-boshiga backup/restore, hatto tenant-boshiga migratsiya vaqtlash imkoniyati ham paydo bo'ladi.

Xarajatlari: migratsiyalar butun flot operatsiyasiga aylanadi — 200 ta tenant'da, bitta deploy 200 ta migratsiyani ishga tushiradi, va yarim yo'lda muvaffaqiyatsizlikka uchraganlar uchun tooling kerak bo'ladi. Prisma bitta klient instansi uchun bitta sxema atrofida qurilgan, shuning uchun siz har bir faol tenant uchun alohida klient (va uning ulanish xarajati)ni boshqarasiz. Bir necha yuzta tenant'da bu haqiqiy muhandislik ishi; bir necha mingtasida esa bu sizning asosiy ishingizga aylanadi.

Biz qanday qaror qabul qilamiz

Bizning standart tanlovimiz: row-level izolatsiya, tenant filtri xotira emas, mexanizm orqali majburlangan holda. Schema-per-tenant faqat qattiq tashqi omil bo'lganda qo'llaniladi — odatda compliance ("bizning ma'lumotlarimiz ajratiladigan bo'lishi kerak"), minglab kichik tenant o'rniga bir nechta yirik tenant, yoki roadmap'da ishonchli on-prem/single-tenant daraja mavjud bo'lganda.

Biz ko'p ko'radigan xato — 50 tenant'li mahsulot uchun bu "o'zini xavfsizroq his qildiradi" degan sababda schema-per-tenant tanlanishi, keyin esa ikki kishilik jamoa mutlaqo qo'llab-quvvatlashga majburiy bo'lmagan migratsiya tooling'ida cho'kib ketishi.

Ishlab chiqarishda o'zini oqlagan Prisma patternlari

Tenant filtrini unutish mumkin bo'lmagan holga keltiring

Asosiy fokus — filtrni inject qiladigan, so'rov ko'lamiga bog'langan (request-scoped) Prisma client kengaytmasi:

// prisma.service.ts — tenant-scoped client via $extends
forTenant(tenantId: string) {
  return this.$extends({
    query: {
      $allModels: {
        async $allOperations({ args, query, operation }) {
          if (TENANT_SCOPED_OPS.has(operation)) {
            args.where = { ...args.where, tenantId };
          }
          return query(args);
        },
      },
    },
  });
}

NestJS'da tenantId'ni guard ichida aniqlang (JWT'dan yoki subdomain'dan — hech qachon request body'dan emas), uni AsyncLocalStorage yoki request-scoped provider'da saqlang, va service'lar scoped (ko'lamlangan) klientni qabul qilsin. Ko'lamlanmagan (unscoped) klient qo'rqinchli nomga ega bitta modulda yashaydi, u faqat admin va billing job'lari tomonidan ishlatiladi. Kod review qoidasi: shu moduldan tashqarida raw klientni import qilishning har qanday holati avtomatik ravishda rad etiladi.

Ishonch va tekshiruv: Postgres RLS

Pul yoki sog'liqqa oid ma'lumotlar bilan ishlaydigan har qanday narsa uchun biz ilova filtri ostiga Postgres row-level security qo'shamiz: CREATE POLICY tenant_isolation ... USING (tenant_id = current_setting('app.tenant_id')::uuid), sozlama har bir tranzaksiyaga qo'llaniladi. Ma'lumot sizib chiqishidan oldin ikkita mustaqil mexanizmning ikkalasi ham muvaffaqiyatsizlikka uchrashi kerak. Bizning workload'larimizdagi o'lchangan qo'shimcha yuk: bir xonali foizning past qismi.

Production'dan olingan saboqlar

  • Kompozit indekslar tenantId bilan boshlanishi shart. (tenantId, createdAt), (tenantId, status). Biz dashboard'ning tenant #80'da 40ms'dan 4s'gacha sekinlashganini ko'rganmiz, chunki indeks tenant ustunini o'z ichiga olmagan va Postgres to'liq skanerlashni tanlagan.
  • Yagonalik (uniqueness) tenant-boshiga bo'lishi kerak. Email uchun @unique emas, @@unique([tenantId, email]). Buni birinchi marta ikkita mijoz ham info@ nomli xodimga ega bo'lganda his qilasiz.
  • Tenant'lararo rad etishni aniq test qiling. Bizning integratsiya to'plamlarimiz ikkita tenant urug'laydi (seed) va har bir list endpoint boshqasidan nol qator qaytarishini tasdiqlaydi. Bu testlar haqiqiy sizib chiqishlarni ikki marta ushlab olgan — ikkalasida ham kengaytmani chetlab o'tgan qo'lda yozilgan raw SQL'da. $queryRaw — izolatsiya o'ladigan joy; har bir holatni audit qiling.
  • Bitta shovqinli tenant sizni topib oladi. Birinchi kunidanoq tenant-boshiga rate limiting va so'rov timeout'lari; kim sekinlik qilayotganini ko'rishingiz uchun metrikangizda tenantId label'i.

Multi-tenancy — bu o'rnatiladigan kutubxona emas. Bu hamma joyda majburlaydigan invariantlar to'plami, va arxitektura savoli aslida shunday: nima orqali majburlanadi — xotira, mexanizm, yoki baza? Kamida mexanizmni tanlang.


Agar shunga o'xshash loyihani rejalashtirayotgan bo'lsangiz, 30 daqiqalik qo'ng'iroqni band qiling — sxemangizni olib keling, biz sizga qaysi model mos kelishi haqida aniq javob beramiz.