{"id":27098,"date":"2026-07-22T10:22:31","date_gmt":"2026-07-22T10:22:31","guid":{"rendered":"https:\/\/umang.pk\/2026\/07\/22\/node-js%d8%8c-rbac%d8%8c-%d8%a7%d9%88%d8%b1-%d8%a2%da%88%d9%b9-%d9%84%d8%a7%da%af%d9%86%da%af-%da%a9%d8%a7-%d8%a7%d8%b3%d8%aa%d8%b9%d9%85%d8%a7%d9%84-%da%a9%d8%b1%d8%aa%db%92-%db%81%d9%88%d8%a6%db%92\/"},"modified":"2026-07-22T10:22:31","modified_gmt":"2026-07-22T10:22:31","slug":"node-js%d8%8c-rbac%d8%8c-%d8%a7%d9%88%d8%b1-%d8%a2%da%88%d9%b9-%d9%84%d8%a7%da%af%d9%86%da%af-%da%a9%d8%a7-%d8%a7%d8%b3%d8%aa%d8%b9%d9%85%d8%a7%d9%84-%da%a9%d8%b1%d8%aa%db%92-%db%81%d9%88%d8%a6%db%92","status":"publish","type":"post","link":"https:\/\/umang.pk\/ur\/2026\/07\/22\/node-js%d8%8c-rbac%d8%8c-%d8%a7%d9%88%d8%b1-%d8%a2%da%88%d9%b9-%d9%84%d8%a7%da%af%d9%86%da%af-%da%a9%d8%a7-%d8%a7%d8%b3%d8%aa%d8%b9%d9%85%d8%a7%d9%84-%da%a9%d8%b1%d8%aa%db%92-%db%81%d9%88%d8%a6%db%92\/","title":{"rendered":"Node.js\u060c RBAC\u060c \u0627\u0648\u0631 \u0622\u0688\u0679 \u0644\u0627\u06af\u0646\u06af \u06a9\u0627 \u0627\u0633\u062a\u0639\u0645\u0627\u0644 \u06a9\u0631\u062a\u06d2 \u06c1\u0648\u0626\u06d2 \u0645\u0644\u0679\u06cc \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 SaaS API \u06a9\u06cc\u0633\u06d2 \u0628\u0646\u0627\u06cc\u0627 \u062c\u0627\u0626\u06d2"},"content":{"rendered":"\n<div id=\"\">\n<p>\u0627\u06cc\u06a9 \u0633\u0627\u062a\u06be\u06cc \u0646\u06d2 \u0645\u062c\u06be \u0633\u06d2 \u06a9\u06c1\u0627 \u06a9\u06c1 \u0627\u0633 \u06a9\u0648 \u0688\u06cc\u0628\u06af \u06a9\u0631\u0646\u06d2 \u0645\u06cc\u06ba \u0645\u062f\u062f \u06a9\u0631\u0648\u06ba \u062c\u0648 \u0627\u0633 \u06a9\u06d2 SaaS \u067e\u0631\u0648\u062c\u06cc\u06a9\u0679 \u0645\u06cc\u0646\u062c\u0645\u0646\u0679 \u0679\u0648\u0644 \u0645\u06cc\u06ba \u0627\u062c\u0627\u0632\u062a \u06a9\u06d2 \u0645\u0633\u0626\u0644\u06d2 \u06a9\u06cc \u0637\u0631\u062d \u0644\u06af\u062a\u0627 \u06c1\u06d2\u06d4 \u0635\u0627\u0631\u0641\u06cc\u0646 \u0648\u06c1 \u0648\u0633\u0627\u0626\u0644 \u062f\u06cc\u06a9\u06be \u0631\u06c1\u06d2 \u062a\u06be\u06d2 \u062c\u0648 \u0627\u0646\u06c1\u0648\u06ba \u0646\u06d2 \u0646\u06c1\u06cc\u06ba \u0628\u0646\u0627\u0626\u06d2 \u062a\u06be\u06d2\u06d4<\/p>\n<p>\u0645\u06cc\u06ba \u0646\u06d2 \u0627\u0633\u062a\u0641\u0633\u0627\u0631 \u0644\u0627\u06af \u0628\u0627\u06c1\u0631 \u0646\u06a9\u0627\u0644\u0627\u060c \u06a9\u0686\u06be \u0679\u06be\u06cc\u06a9 \u0679\u06be\u06cc\u06a9 \u06a9\u06cc \u0627\u0645\u06cc\u062f \u0645\u06cc\u06ba\u06d4 \u0627\u06cc\u0633\u0627 \u0646\u06c1\u06cc\u06ba \u062a\u06be\u0627\u06d4 \u0641\u06c1\u0631\u0633\u062a \u06a9\u06d2 \u0627\u062e\u062a\u062a\u0627\u0645\u06cc \u0646\u0642\u0637\u06c1 \u0645\u06cc\u06ba \u0646\u06c1\u06cc\u06ba \u06c1\u06d2\u06d4 <code>tenant_id<\/code> \u0628\u0627\u0644\u06a9\u0644 \u0628\u06be\u06cc \u0641\u0644\u0679\u0631 \u0646\u06c1 \u06a9\u0631\u06cc\u06ba\u06d4 \u0688\u06cc\u0679\u0627 \u0628\u06cc\u0633 \u0645\u06cc\u06ba \u06a9\u0648\u0626\u06cc \u0628\u06be\u06cc \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u0633\u06cc \u062f\u0648\u0633\u0631\u06d2 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06d2 \u0645\u0646\u0635\u0648\u0628\u0648\u06ba \u06a9\u0648 \u067e\u0691\u06be \u0633\u06a9\u062a\u0627 \u06c1\u06d2\u06d4 \u062f\u0631\u062e\u0648\u0627\u0633\u062a \u0645\u06cc\u06ba \u06a9\u0648\u0626\u06cc \u063a\u0644\u0637\u06cc \u0646\u06c1\u06cc\u06ba \u06c1\u0648\u0626\u06cc\u06d4 \u06cc\u06c1 \u0635\u0631\u0641 \u0648\u06c1\u06cc \u0648\u0627\u067e\u0633 \u0622\u06cc\u0627 \u062c\u0648 \u0648\u06c1\u0627\u06ba \u062a\u06be\u0627\u06d4<\/p>\n<p>\u0644\u0627\u067e\u062a\u06c1 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0641\u0644\u0679\u0631\u0632 \u062e\u0631\u0627\u0628\u06cc \u06a9\u0627 \u0633\u0628\u0628 \u0646\u06c1\u06cc\u06ba \u0628\u0646\u062a\u06d2 \u06c1\u06cc\u06ba\u06d4 \u06cc\u06c1 \u0628\u063a\u06cc\u0631 \u06a9\u0633\u06cc \u0634\u06a9\u0627\u06cc\u062a \u06a9\u06d2 \u063a\u0644\u0637 \u0688\u06cc\u0679\u0627 \u0648\u0627\u067e\u0633 \u06a9\u0631\u062a\u0627 \u06c1\u06d2 \u0627\u0648\u0631 \u0644\u0627\u06af\u0632 \u0645\u06cc\u06ba \u062c\u06be\u0646\u0688\u0627 \u0646\u06c1\u06cc\u06ba \u0644\u06af\u0627\u06cc\u0627 \u062c\u0627\u062a\u0627 \u06c1\u06d2\u06d4 \u0645\u06cc\u06ba \u0646\u06d2 \u0627\u0633\u06d2 \u06a9\u0686\u06be \u06c1\u0641\u062a\u0648\u06ba \u062a\u06a9 \u067e\u0631\u0648\u0688\u06a9\u0634\u0646 \u0645\u06cc\u06ba \u0686\u0644\u062a\u06d2 \u062f\u06cc\u06a9\u06be\u0627 \u0627\u0633 \u0633\u06d2 \u067e\u06c1\u0644\u06d2 \u06a9\u06c1 \u0633\u067e\u0648\u0631\u0679 \u0679\u06a9\u0679 \u0646\u06d2 \u0627\u0633\u062a\u0641\u0633\u0627\u0631 \u06a9\u06d2 \u0644\u0627\u06af \u0645\u06cc\u06ba \u06a9\u0633\u06cc \u06a9\u06cc \u0646\u0634\u0627\u0646\u062f\u06c1\u06cc \u06a9\u06cc\u06d4<\/p>\n<p>\u0627\u06cc\u06a9 \u0628\u0627\u0631 \u062c\u0628 \u06cc\u06c1 \u0645\u0646\u0638\u0631 \u0639\u0627\u0645 \u067e\u0631 \u0622\u062c\u0627\u0626\u06d2 \u062a\u0648 \u06cc\u06c1 \u0628\u06c1\u062a \u0627\u06c1\u0645 \u06c1\u06d2 \u06a9\u06c1 \u0627\u0633\u06d2 \u067e\u06c1\u0644\u06d2 \u06a9\u0648\u0646 \u062f\u0631\u06cc\u0627\u0641\u062a \u06a9\u0631\u062a\u0627 \u06c1\u06d2\u06d4 \u0648\u06c1 \u0635\u0627\u0631\u0641\u06cc\u0646 \u062c\u0648 \u0627\u0633 \u06a9\u0648 \u0628\u0631\u0627 \u0633\u0645\u062c\u06be\u062a\u06d2 \u06c1\u06cc\u06ba\u06d4 \u0627\u06cc\u06a9 \u06a9\u0645\u067e\u0644\u0627\u0626\u0646\u0633 \u0622\u0688\u06cc\u0679\u0631 \u062c\u0648 SOC 2 \u06a9\u06d2 \u062c\u0627\u0626\u0632\u06d2 \u06a9\u06d2 \u062f\u0648\u0631\u0627\u0646 \u0627\u0633 \u06a9\u0627 \u0646\u0648\u0679\u0633 \u0644\u06d2 \u0631\u06c1\u0627 \u06c1\u06d2 \u0627\u06cc\u06a9 \u0645\u062e\u062a\u0644\u0641 \u0642\u0633\u0645 \u06a9\u0627 \u0645\u0633\u0626\u0644\u06c1 \u06c1\u06d2\u06d4<\/p>\n<p>\u0634\u0631\u0648\u0639 \u0633\u06d2 \u06c1\u06cc \u062a\u0646\u06c1\u0627\u0626\u06cc \u06a9\u0627\u0645 \u06a9\u0627 \u062f\u0646 \u06c1\u06d2\u06d4 \u0679\u06cc\u0645 \u0646\u06d2 \u062a\u0639\u0645\u06cc\u0644 \u06a9\u06d2 \u062c\u0627\u0626\u0632\u06d2 \u06a9\u06d2 \u0628\u0639\u062f \u0627\u0633\u06d2 \u0628\u06c1\u062a\u0631 \u0628\u0646\u0627\u0646\u06d2 \u0645\u06cc\u06ba \u0628\u06c1\u062a \u0632\u06cc\u0627\u062f\u06c1 \u0648\u0642\u062a \u0635\u0631\u0641 \u06a9\u06cc\u0627\u060c \u0627\u0648\u0631 \u0627\u0633 \u0645\u06cc\u06ba \u06a9\u0633\u06cc \u0627\u0648\u0631 \u06a9\u06d2 \u0645\u0642\u0627\u0628\u0644\u06d2 \u0645\u06cc\u06ba \u0632\u06cc\u0627\u062f\u06c1 \u0635\u0627\u0631\u0641\u06cc\u0646 \u06a9\u06cc \u0627\u06cc \u0645\u06cc\u0644\u0632 \u0634\u0627\u0645\u0644 \u062a\u06be\u06cc\u06ba\u06d4<\/p>\n<p>\u0627\u0633\u0679\u06cc\u06a9 PostgreSQL \u06a9\u06d2 \u0633\u0627\u062a\u06be Node.js \u06c1\u06d2\u06d4 CRUD \u0622\u0633\u0627\u0646 \u062d\u0635\u06c1 \u06c1\u06d2\u06d4 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631\u0648\u06ba \u06a9\u06cc \u062a\u0646\u06c1\u0627\u0626\u06cc\u060c RBAC\u060c \u0627\u0648\u0631 \u0622\u0688\u0679 \u0644\u0627\u06af\u0646\u06af \u067e\u0631 \u0632\u06cc\u0627\u062f\u06c1 \u062a\u0648\u062c\u06c1 \u062f\u06cc\u0646\u06d2 \u06a9\u06cc \u0636\u0631\u0648\u0631\u062a \u06c1\u0648\u062a\u06cc \u06c1\u06d2\u060c \u0627\u0648\u0631 \u062c\u06c1\u0627\u06ba \u0648\u06c1 \u0686\u06cc\u06a9 \u0627\u0633\u0679\u06cc\u06a9 \u0645\u06cc\u06ba \u0686\u0644\u062a\u06d2 \u06c1\u06cc\u06ba \u06cc\u06c1 \u0627\u06c1\u0645 \u06c1\u06d2\u06d4 \u0631\u0648\u0679 \u06c1\u06cc\u0646\u0688\u0644\u0631 \u06a9\u06d2 \u0686\u0644\u0646\u06d2 \u0633\u06d2 \u067e\u06c1\u0644\u06d2 \u0645\u06cc\u06ba \u0646\u06d2 \u062a\u06cc\u0646\u0648\u06ba \u06a9\u0648 \u0645\u0688\u0644 \u0648\u06cc\u0626\u0631 \u0645\u06cc\u06ba \u0688\u0627\u0644 \u062f\u06cc\u0627\u06d4 \u06c1\u06cc\u0646\u0688\u0644\u0631\u0632 \u062c\u0648 \u062a\u0646\u06c1\u0627\u0626\u06cc \u06a9\u06cc \u0645\u0646\u0637\u0642 \u06a9\u0648 \u0628\u0631\u0627\u06c1 \u0631\u0627\u0633\u062a \u0646\u06c1\u06cc\u06ba \u06a9\u06c1\u062a\u06d2 \u06c1\u06cc\u06ba \u0648\u06c1 \u063a\u0644\u0637\u06cc \u0633\u06d2 \u062a\u0646\u06c1\u0627\u0626\u06cc \u06a9\u06cc \u0645\u0646\u0637\u0642 \u06a9\u0648 \u0646\u06c1\u06cc\u06ba \u0686\u06be\u0648\u0691 \u0633\u06a9\u062a\u06d2 \u06c1\u06cc\u06ba\u06d4<\/p>\n<h2 id=\"heading-prerequisites\">\u0634\u0631\u0637\u06cc\u06ba<\/h2>\n<h2 id=\"heading-what-we-will-build\">\u062c\u0648 \u06c1\u0645 \u062a\u0639\u0645\u06cc\u0631 \u06a9\u0631\u06cc\u06ba \u06af\u06d2\u06d4<\/h2>\n<p>\u0645\u0644\u0679\u06cc \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0627\u06cc\u06a9\u0633\u067e\u0631\u06cc\u0633 REST API \u062c\u0648 \u0644\u0627\u06af\u0648 \u06c1\u0648\u062a\u0627 \u06c1\u06d2:<\/p>\n<ol>\n<li>\n<p><strong>\u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06cc \u062a\u0646\u06c1\u0627\u0626\u06cc:<\/strong> \u062a\u0645\u0627\u0645 \u0688\u06cc\u0679\u0627 \u0628\u06cc\u0633 \u0633\u0648\u0627\u0644\u0627\u062a \u06a9\u0627 \u062f\u0627\u0626\u0631\u06c1 \u06a9\u0627\u0631 \u06c1\u06d2\u06d4 <code>tenant_id<\/code> \u0627\u06cc\u06a9 \u062a\u0648\u062b\u06cc\u0642 \u0634\u062f\u06c1 JWT \u0633\u06d2\u06d4 \u06a9\u0644\u0627\u0626\u0646\u0679\u0633 \u0627\u0633 \u0628\u0627\u062a \u067e\u0631 \u0627\u062b\u0631 \u0627\u0646\u062f\u0627\u0632 \u0646\u06c1\u06cc\u06ba \u06c1\u0648 \u0633\u06a9\u062a\u06d2 \u06a9\u06c1 \u06a9\u0648\u0646 \u0633\u06d2 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0627\u0633\u062a\u0641\u0633\u0627\u0631 \u06a9\u0631\u062a\u0627 \u06c1\u06d2\u06d4<\/p>\n<\/li>\n<li>\n<p><strong>\u0622\u0631 \u0628\u06cc \u0627\u06d2 \u0633\u06cc:<\/strong> \u0686\u0627\u0631 \u06a9\u0631\u062f\u0627\u0631 \u06c1\u06cc\u06ba\u060c \u06c1\u0631 \u0627\u06cc\u06a9 \u0639\u062f\u062f\u06cc \u0633\u0637\u062d \u06a9\u06d2 \u0633\u0627\u062a\u06be (SuperAdmin \u0633\u0628 \u0633\u06d2 \u0632\u06cc\u0627\u062f\u06c1 \u06c1\u06d2\u060c \u0646\u0627\u0638\u0631\u06cc\u0646 \u0633\u0628 \u0633\u06d2 \u06a9\u0645 \u06c1\u06d2)\u06d4 \u0645\u0688\u0644 \u0648\u06cc\u0626\u0631 \u06c1\u06cc\u0646\u0688\u0644\u0631 \u06a9\u0648 \u067e\u06be\u0627\u0646\u0633\u06cc \u062f\u06cc\u0646\u06d2 \u0633\u06d2 \u067e\u06c1\u0644\u06d2 \u0633\u0637\u062d \u06a9\u06cc \u062c\u0627\u0646\u0686 \u06a9\u0631\u062a\u0627 \u06c1\u06d2\u06d4<\/p>\n<\/li>\n<li>\n<p><strong>\u0622\u0688\u0679 \u0644\u0627\u06af\u0646\u06af:<\/strong> \u062a\u062d\u0631\u06cc\u0631 \u06cc\u0627 \u062a\u0646\u0642\u06cc\u062f\u06cc \u067e\u0691\u06be\u0646\u0627 \u0622\u0688\u0679 \u0679\u06cc\u0628\u0644 \u0645\u06cc\u06ba \u0627\u06cc\u06a9 \u0642\u0637\u0627\u0631 \u06a9\u0627 \u0627\u0636\u0627\u0641\u06c1 \u06a9\u0631\u062a\u0627 \u06c1\u06d2\u06d4 \u0627\u06cc\u067e \u0628\u0639\u062f \u0645\u06cc\u06ba \u0627\u0633 \u0642\u0637\u0627\u0631 \u0645\u06cc\u06ba \u062a\u0631\u0645\u06cc\u0645 \u0646\u06c1\u06cc\u06ba \u06a9\u0631 \u0633\u06a9\u062a\u06cc\u06d4 \u0688\u06cc\u0679\u0627 \u0628\u06cc\u0633 \u0627\u0633\u06d2 \u0628\u0631\u0627\u06c1 \u0631\u0627\u0633\u062a \u0646\u0627\u0641\u0630 \u06a9\u0631\u062a\u0627 \u06c1\u06d2\u06d4 \u0627\u06cc\u067e \u0645\u06cc\u06ba \u0627\u06cc\u06a9 \u0628\u06af \u06a9\u06cc \u0648\u062c\u06c1 \u0633\u06d2\u060c \u0627\u06af\u0631 \u0622\u067e \u0622\u0688\u0679 \u0642\u0637\u0627\u0631 \u06a9\u0648 \u0627\u067e \u0688\u06cc\u0679 \u06a9\u0631\u0646\u06d2 \u06a9\u06cc \u06a9\u0648\u0634\u0634 \u06a9\u0631\u062a\u06d2 \u06c1\u06cc\u06ba\u060c \u062a\u0648 \u0688\u06cc\u0679\u0627 \u0628\u06cc\u0633 \u0627\u0633\u06d2 \u0645\u0633\u062a\u0631\u062f \u06a9\u0631 \u062f\u06d2 \u06af\u0627\u06d4 \u0635\u0631\u0641 \u062f\u0631\u062e\u0648\u0627\u0633\u062a \u06a9\u06cc \u0633\u0637\u062d \u06a9\u0627 \u0646\u0641\u0627\u0630 \u06cc\u06c1 \u0636\u0645\u0627\u0646\u062a\u06cc\u06ba \u0641\u0631\u0627\u06c1\u0645 \u0646\u06c1\u06cc\u06ba \u06a9\u0631 \u0633\u06a9\u062a\u0627\u06d4<\/p>\n<\/li>\n<li>\n<p><strong>\u0641\u06cc \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0634\u0631\u062d \u06a9\u06cc \u062d\u062f:<\/strong> \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06d2 \u0645\u0637\u0627\u0628\u0642 \u0631\u06cc\u0688\u06cc\u0633 \u06a9\u06cc \u062f\u0631\u062e\u0648\u0627\u0633\u062a\u0648\u06ba \u06a9\u06cc \u062a\u0639\u062f\u0627\u062f\u06d4 \u0645\u06cc\u06ba \u0646\u06d2 \u0622\u0626\u06cc \u067e\u06cc \u067e\u0631 \u0645\u0628\u0646\u06cc \u067e\u0627\u0628\u0646\u062f\u06cc\u0648\u06ba \u06a9\u06cc \u0648\u062c\u06c1 \u0633\u06d2 \u06a9\u0627\u0631\u067e\u0648\u0631\u06cc\u0679 \u0631\u0648\u0644 \u0622\u0624\u0679 \u0631\u06a9\u06d2 \u06c1\u0648\u0626\u06d2 \u062f\u06cc\u06a9\u06be\u0627 \u06c1\u06d2 \u062c\u0628 50 \u0635\u0627\u0631\u0641\u06cc\u0646 \u0627\u06cc\u06a9 \u06c1\u06cc \u06a9\u0627\u0631\u067e\u0648\u0631\u06cc\u0679 \u067e\u0631\u0627\u06a9\u0633\u06cc \u06a9\u06d2 \u0630\u0631\u06cc\u0639\u06d2 \u062c\u0691\u06d2 \u06c1\u0648\u0626\u06d2 \u062a\u06be\u06d2\u06d4<\/p>\n<\/li>\n<li>\n<p><strong>\u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0627\u0644\u06af \u062a\u06be\u0644\u06af \u0679\u06cc\u0633\u0679:<\/strong> \u06cc\u06c1 \u0627\u06cc\u06a9 \u0633\u0631\u0634\u0627\u0631 \u0679\u06cc\u0633\u0679 \u0641\u0627\u0626\u0644 \u06c1\u06d2 \u062c\u0648 \u062b\u0627\u0628\u062a \u06a9\u0631\u062a\u06cc \u06c1\u06d2 \u06a9\u06c1 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631\u0648\u06ba \u06a9\u06d2 \u062f\u0631\u0645\u06cc\u0627\u0646 \u0688\u06cc\u0679\u0627 \u0644\u06cc\u06a9 \u0646\u06c1\u06cc\u06ba \u06a9\u06cc\u0627 \u062c\u0627 \u0633\u06a9\u062a\u0627\u06d4 \u0627\u0633\u06d2 CI \u0633\u06d2 \u062c\u0648\u0691\u0646\u06d2 \u0633\u06d2 \u0627\u0633\u06d2 \u0628\u06be\u06cc\u062c\u0646\u06d2 \u0633\u06d2 \u067e\u06c1\u0644\u06d2 \u0679\u0648\u0679\u0627 \u06c1\u0648\u0627 \u062a\u0646\u06c1\u0627\u0626\u06cc \u067e\u06a9\u0691\u062a\u0627 \u06c1\u06d2\u06d4<\/p>\n<\/li>\n<\/ol>\n<h2 id=\"heading-table-of-contents\">\u0627\u0646\u0688\u06cc\u06a9\u0633<\/h2>\n<ol>\n<li>\n<p>\u06a9\u062b\u06cc\u0631 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631\u06cc \u06a9\u06cc\u0633\u06d2 \u06a9\u0627\u0645 \u06a9\u0631\u062a\u06cc \u06c1\u06d2\u06d4<\/p>\n<\/li>\n<li>\n<p>\u0641\u0646 \u062a\u0639\u0645\u06cc\u0631 \u06a9\u0627 \u062c\u0627\u0626\u0632\u06c1<\/p>\n<\/li>\n<li>\n<p>\u0688\u06cc\u0679\u0627 \u0628\u06cc\u0633 \u0627\u0633\u06a9\u06cc\u0645\u0627 \u0688\u06cc\u0632\u0627\u0626\u0646<\/p>\n<\/li>\n<li>\n<p>\u067e\u0631\u0648\u062c\u06cc\u06a9\u0679 \u06a9\u06cc \u062a\u0631\u062a\u06cc\u0628\u0627\u062a<\/p>\n<\/li>\n<li>\n<p>\u06a9\u062b\u06cc\u0631 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631\u06cc \u06a9\u06d2 \u0644\u06cc\u06d2 JWT \u0688\u06cc\u0632\u0627\u0626\u0646<\/p>\n<\/li>\n<li>\n<p>\u062a\u0648\u062b\u06cc\u0642 \u0627\u0648\u0631 RBAC \u0645\u0688\u0644 \u0648\u06cc\u0626\u0631<\/p>\n<\/li>\n<li>\n<p>\u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06d2 \u0644\u06cc\u06d2 \u0645\u062d\u0641\u0648\u0638 \u0630\u062e\u06cc\u0631\u06c1 \u06a9\u06cc \u067e\u0631\u062a<\/p>\n<\/li>\n<li>\n<p>\u0622\u0688\u0679 \u0644\u0627\u06af\u0646\u06af \u0633\u0631\u0648\u0633<\/p>\n<\/li>\n<li>\n<p>\u0641\u06cc \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0634\u0631\u062d \u06a9\u06cc \u062d\u062f<\/p>\n<\/li>\n<li>\n<p>\u0631\u0627\u0633\u062a\u06d2 \u06a9\u06cc \u062a\u0639\u0645\u06cc\u0631<\/p>\n<\/li>\n<li>\n<p>\u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u062a\u0646\u06c1\u0627\u0626\u06cc \u06a9\u06cc \u062c\u0627\u0646\u0686<\/p>\n<\/li>\n<li>\n<p>\u0645\u0633\u0626\u0644\u06c1 \u062d\u0644<\/p>\n<\/li>\n<li>\n<p>\u062e\u062a\u0645<\/p>\n<\/li>\n<\/ol>\n<h2 id=\"heading-how-multi-tenancy-works\">\u06a9\u062b\u06cc\u0631 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631\u06cc \u06a9\u06cc\u0633\u06d2 \u06a9\u0627\u0645 \u06a9\u0631\u062a\u06cc \u06c1\u06d2\u06d4<\/h2>\n<p>\u0627\u0633 \u0679\u06cc\u0648\u0679\u0648\u0631\u06cc\u0644 \u0645\u06cc\u06ba <strong>\u0642\u0637\u0627\u0631 \u06a9\u06cc \u0633\u0637\u062d \u06a9\u06cc \u062a\u0646\u06c1\u0627\u0626\u06cc \u06a9\u06d2 \u0633\u0627\u062a\u06be \u0645\u0634\u062a\u0631\u06a9\u06c1 \u0688\u06cc\u0679\u0627 \u0628\u06cc\u0633<\/strong>: \u0633\u06d2 <code>tenant_id<\/code> \u06c1\u0631 \u062c\u062f\u0648\u0644 \u0645\u06cc\u06ba \u06a9\u0627\u0644\u0645\u060c \u06c1\u0631 \u0633\u0648\u0627\u0644 \u0645\u06cc\u06ba \u0641\u0644\u0679\u0631\u06d4 \u0627\u06cc\u06a9 \u0688\u06cc\u0679\u0627 \u0628\u06cc\u0633 \u06c1\u0631 \u06a9\u0633\u06cc \u06a9\u0627 \u0688\u06cc\u0679\u0627 \u0627\u06a9\u0679\u06be\u0627 \u0631\u06a9\u06be\u062a\u0627 \u06c1\u06d2\u06d4 \u062f\u0631\u062e\u0648\u0627\u0633\u062a \u0627\u0633 \u0628\u0627\u062a \u06a9\u0627 \u062a\u0639\u06cc\u0646 \u06a9\u0631\u062a\u06cc \u06c1\u06d2 \u06a9\u06c1 \u06c1\u0631 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06cc\u0627 \u062f\u06cc\u06a9\u06be \u0633\u06a9\u062a\u0627 \u06c1\u06d2\u06d4<\/p>\n<p>\u062f\u0648 \u0645\u062e\u062a\u0644\u0641 \u0637\u0631\u06cc\u0642\u06d2 \u06c1\u06cc\u06ba: \u0641\u06cc \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0627\u0633\u06a9\u06cc\u0645\u0627 \u0627\u0648\u0631 \u0641\u06cc \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0688\u06cc\u0679\u0627 \u0628\u06cc\u0633\u06d4 \u0645\u06cc\u06ba \u0646\u06d2 \u0679\u06cc\u0645 \u06a9\u06d2 \u0633\u0627\u062a\u06be \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06d2 \u0645\u062e\u0635\u0648\u0635 \u0627\u0633\u06a9\u06cc\u0645\u0648\u06ba \u06a9\u06d2 \u0628\u0627\u0631\u06d2 \u0645\u06cc\u06ba \u0628\u0627\u062a \u06a9\u06cc \u062c\u0633 \u06a9\u06d2 \u0646\u062a\u06cc\u062c\u06d2 \u0645\u06cc\u06ba \u06c1\u0645 \u0646\u06d2 \u062d\u0642\u06cc\u0642\u06cc \u0645\u0635\u0646\u0648\u0639\u0627\u062a \u06a9\u06d2 \u0645\u0642\u0627\u0628\u0644\u06d2 \u0645\u06cc\u06ba \u0645\u0646\u062a\u0642\u0644\u06cc \u06a9\u06d2 \u0622\u0644\u0627\u062a \u067e\u0631 \u0627\u0646\u062c\u06cc\u0646\u0626\u0631\u0646\u06af \u06a9\u0627 \u0632\u06cc\u0627\u062f\u06c1 \u0648\u0642\u062a \u0635\u0631\u0641 \u06a9\u06cc\u0627\u06d4 \u0641\u06cc \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0688\u06cc\u0679\u0627 \u0628\u06cc\u0633 \u0645\u0636\u0628\u0648\u0637 \u0636\u0645\u0627\u0646\u062a\u06cc\u06ba \u0641\u0631\u0627\u06c1\u0645 \u06a9\u0631\u062a\u06d2 \u06c1\u06cc\u06ba\u060c \u0644\u06cc\u06a9\u0646 \u062c\u0628 \u0628\u06be\u06cc \u06a9\u0648\u0626\u06cc \u0646\u06cc\u0627 \u06af\u0627\u06c1\u06a9 \u0633\u0627\u0626\u0646 \u0627\u067e \u06a9\u0631\u062a\u0627 \u06c1\u06d2 \u062a\u0648 \u06a9\u0646\u06a9\u0634\u0646 \u067e\u0648\u0644 \u067e\u06be\u06cc\u0644\u062a\u0627 \u06c1\u06d2\u06d4<\/p>\n<p>\u0646\u06c1 \u06c1\u06cc \u0633\u0633\u062a\u06d2 \u062a\u0631\u0627\u0632\u0648\u06d4 \u0642\u0637\u0627\u0631 \u06a9\u06cc \u0633\u0637\u062d \u06a9\u06cc \u062a\u0646\u06c1\u0627\u0626\u06cc \u0632\u06cc\u0627\u062f\u06c1 \u062a\u0631 \u0679\u06cc\u0645\u0648\u06ba \u06a9\u06cc \u062a\u0648\u0642\u0639 \u0633\u06d2 \u06a9\u06c1\u06cc\u06ba \u0632\u06cc\u0627\u062f\u06c1 \u0628\u0691\u06be \u062c\u0627\u062a\u06cc \u06c1\u06d2\u06d4 \u06a9\u0686\u06be \u0644\u0648\u06af \u062c\u0646 \u06a9\u0648 \u0645\u06cc\u06ba \u062c\u0627\u0646\u062a\u0627 \u06c1\u0648\u06ba \u0648\u06c1 \u0628\u0631\u0633\u0648\u06ba \u0633\u06d2 \u0627\u06cc\u0633\u0627 \u06a9\u0631 \u0631\u06c1\u06d2 \u06c1\u06cc\u06ba\u060c \u0627\u0633 \u0644\u06cc\u06d2 \u0646\u06c1\u06cc\u06ba \u06a9\u06c1 \u0637\u0631\u06cc\u0642\u06c1 \u06a9\u0627\u0631 \u0628\u0646\u062f \u06a9\u0631 \u062f\u06cc\u0627 \u06af\u06cc\u0627 \u06c1\u06d2\u060c \u0628\u0644\u06a9\u06c1 \u0639\u0627\u0645 \u0637\u0648\u0631 \u067e\u0631 \u06a9\u0686\u06be \u0631\u06cc\u06af\u0648\u0644\u06cc\u0679\u0631\u06cc \u062f\u0628\u0627\u0624 \u06a9\u06d2 \u062a\u062d\u062a\u06d4<\/p>\n<p>\u0627\u06cc\u06a9 \u0686\u06cc\u0632 \u062c\u0648 \u0627\u0633 \u0688\u06cc\u0632\u0627\u0626\u0646 \u0645\u06cc\u06ba \u0627\u062e\u062a\u06cc\u0627\u0631\u06cc \u0646\u06c1\u06cc\u06ba \u06c1\u06d2 \u0648\u06c1 \u06c1\u06d2: <code>tenant_id<\/code> <strong>\u06cc\u06c1 \u06c1\u0645\u06cc\u0634\u06c1 \u062a\u0635\u062f\u06cc\u0642 \u0634\u062f\u06c1 JWT \u0633\u06d2 \u0622\u0646\u0627 \u0686\u0627\u06c1\u0626\u06d2\u06d4<\/strong> \u062f\u0631\u062e\u0648\u0627\u0633\u062a \u06a9\u0627 \u0628\u0627\u0688\u06cc \u06cc\u0627 URL \u0646\u06c1\u06cc\u06ba\u06d4 \u0635\u0627\u0631\u0641 \u06a9\u0646\u0679\u0631\u0648\u0644 \u06a9\u0631\u062a\u0627 \u06c1\u06d2 \u06a9\u06c1 \u062f\u0648\u0646\u0648\u06ba \u0645\u06cc\u06ba \u06a9\u06cc\u0627 \u062c\u0627\u062a\u0627 \u06c1\u06d2\u06d4 \u06cc\u06c1 \u0633\u0631\u0648\u0631 \u067e\u0631 JWT \u0645\u06cc\u06ba \u0644\u0627\u06af \u0627\u0646 \u06c1\u0648\u0646\u06d2 \u06a9\u0648 \u06a9\u0646\u0679\u0631\u0648\u0644 \u0646\u06c1\u06cc\u06ba \u06a9\u0631\u062a\u0627 \u06c1\u06d2\u06d4<\/p>\n<h2 id=\"heading-architecture-overview\">\u0641\u0646 \u062a\u0639\u0645\u06cc\u0631 \u06a9\u0627 \u062c\u0627\u0626\u0632\u06c1<\/h2>\n<pre><code class=\"language-plaintext\">HTTP Request\n     \u2502\n     \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502           Express Middleware Stack       \u2502\n\u2502                                         \u2502\n\u2502  1. Rate Limiter (per tenant_id)        \u2502\n\u2502  2. Auth Middleware (verify JWT)        \u2502\n\u2502     \u2514\u2500\u25ba Extracts: userId, tenantId,    \u2502\n\u2502          role, permissions              \u2502\n\u2502  3. RBAC Middleware (check role)        \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n               \u2502\n               \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502           Route Handler                  \u2502\n\u2502                                         \u2502\n\u2502  1. Call Repository (tenant-safe query) \u2502\n\u2502  2. Call Audit Service (fire & forget)  \u2502\n\u2502  3. Return response                     \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n               \u2502\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n     \u25bc                    \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Projects\u2502        \u2502 Audit Logs \u2502\n\u2502  Table  \u2502        \u2502   Table    \u2502\n\u2502(+tenant)\u2502        \u2502(append only\u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre>\n<p>\u06c1\u06cc\u0646\u0688\u0644\u0631 \u06a9\u06cc \u062f\u0631\u062e\u0648\u0627\u0633\u062a \u06a9\u06cc \u062a\u0648\u062b\u06cc\u0642 \u06a9\u0631\u0646\u06d2 \u0633\u06d2 \u067e\u06c1\u0644\u06d2 \u0634\u0631\u062d \u06a9\u06cc \u062d\u062f \u0628\u0646\u062f\u06cc\u060c \u062a\u0635\u062f\u06cc\u0642\u060c \u0627\u0648\u0631 RBAC \u0633\u0628\u06be\u06cc \u06a9\u0648 \u0639\u0645\u0644 \u0645\u06cc\u06ba \u0644\u0627\u06cc\u0627 \u062c\u0627\u062a\u0627 \u06c1\u06d2\u06d4 \u062a\u062d\u0631\u06cc\u0631 \u0622\u0688\u0679 \u0633\u0631\u0648\u0633 \u0633\u06d2 \u06af\u0632\u0631\u062a\u06cc \u06c1\u06d2\u06d4 \u0645\u062e\u0632\u0646 \u06c1\u06d2\u06d4 <code>tenantId<\/code> \u0633\u06d2 <code>req.user<\/code> \u06c1\u06cc\u0646\u0688\u0644\u0631 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0627\u0633\u06a9\u0648\u067e\u0646\u06af \u06a9\u0648 \u0628\u0631\u0627\u06c1 \u0631\u0627\u0633\u062a \u0646\u06c1\u06cc\u06ba \u0686\u06be\u0648\u062a\u0627 \u06c1\u06d2\u060c \u0644\u06c1\u0630\u0627 \u0627\u0633 \u06a9\u06d2 \u0622\u0633 \u067e\u0627\u0633 \u06a9\u0648\u0626\u06cc \u0631\u0627\u0633\u062a\u06c1 \u0646\u06c1\u06cc\u06ba \u06c1\u06d2\u06d4<\/p>\n<h2 id=\"heading-database-schema-design\">\u0688\u06cc\u0679\u0627 \u0628\u06cc\u0633 \u0627\u0633\u06a9\u06cc\u0645\u0627 \u0688\u06cc\u0632\u0627\u0626\u0646<\/h2>\n<pre><code class=\"language-sql\">-- Tenants table\nCREATE TABLE tenants (\n  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n  name        VARCHAR(255) NOT NULL,\n  plan        VARCHAR(50) NOT NULL DEFAULT 'free', -- 'free', 'pro', 'enterprise'\n  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\n-- Users table\nCREATE TABLE users (\n  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n  tenant_id   UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,\n  email       VARCHAR(255) NOT NULL,\n  role        VARCHAR(50) NOT NULL DEFAULT 'Member', -- 'SuperAdmin','TenantAdmin','Member','Viewer'\n  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n  UNIQUE(tenant_id, email)\n);\n\nCREATE INDEX idx_users_tenant ON users(tenant_id);\n\n-- Projects table (example resource \u2014 replace with your domain entity)\nCREATE TABLE projects (\n  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n  tenant_id   UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,\n  name        VARCHAR(255) NOT NULL,\n  description TEXT,\n  created_by  UUID NOT NULL REFERENCES users(id),\n  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n  updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX idx_projects_tenant ON projects(tenant_id);\n\n-- Audit log table (append-only \u2014 never UPDATE or DELETE rows here)\nCREATE TABLE audit_logs (\n  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n  tenant_id   UUID NOT NULL,\n  user_id     UUID NOT NULL,\n  user_email  TEXT NOT NULL,\n  user_role   TEXT NOT NULL,        -- role at time of action\n  action      TEXT NOT NULL,        -- 'CREATE', 'UPDATE', 'DELETE', 'VIEW'\n  resource    TEXT NOT NULL,        -- table name\n  resource_id TEXT,\n  old_values  JSONB,\n  new_values  JSONB,\n  ip_address  INET,\n  user_agent  TEXT,\n  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX idx_audit_tenant ON audit_logs(tenant_id);\nCREATE INDEX idx_audit_created ON audit_logs(created_at DESC);\n\n-- Protect audit log at database level\n-- Use a DO block so this runs safely in Docker where app_user is the superuser\nDO $$\nBEGIN\n  IF current_user <> 'app_user' THEN\n    REVOKE DELETE, UPDATE ON audit_logs FROM app_user;\n  END IF;\nEND $$;\n<\/code><\/pre>\n<p>\u06a9\u06c1 <code>REVOKE<\/code> \u06cc\u06c1 \u0636\u0631\u0648\u0631\u06cc \u06c1\u06d2\u06d4 \u0627\u06cc\u067e\u0644\u06cc\u06a9\u06cc\u0634\u0646 \u06a9\u06cc\u0691\u06d2 \u067e\u06cc\u062f\u0627 \u06c1\u0648\u062a\u06d2 \u06c1\u06cc\u06ba\u06d4 \u0627\u06af\u0631 \u0622\u067e \u06a9\u06d2 \u06a9\u0648\u0688\u0628\u06cc\u0633 \u0645\u06cc\u06ba \u06a9\u0648\u0626\u06cc \u0686\u06cc\u0632 \u063a\u0644\u0637\u06cc \u0633\u06d2 \u0622\u0688\u0679 \u0642\u0637\u0627\u0631 \u06a9\u0648 \u0627\u067e \u0688\u06cc\u0679 \u06a9\u0631\u0646\u06d2 \u06a9\u06cc \u06a9\u0648\u0634\u0634 \u06a9\u0631\u062a\u06cc \u06c1\u06d2\u060c \u062a\u0648 \u0622\u067e \u0646\u06c1\u06cc\u06ba \u0686\u0627\u06c1\u062a\u06d2 \u06a9\u06c1 \u0688\u06cc\u0679\u0627 \u0628\u06cc\u0633 \u062e\u0648\u062f \u0628\u062e\u0648\u062f \u0627\u0633 \u06a9\u06cc \u067e\u06cc\u0631\u0648\u06cc \u06a9\u0631\u06d2 \u0627\u0648\u0631 \u0627\u0633\u06d2 \u06cc\u06a9\u0633\u0631 \u0645\u0633\u062a\u0631\u062f \u06a9\u0631\u06d2\u06d4<\/p>\n<h2 id=\"heading-project-setup\">\u067e\u0631\u0648\u062c\u06cc\u06a9\u0679 \u06a9\u06cc \u062a\u0631\u062a\u06cc\u0628\u0627\u062a<\/h2>\n<pre><code class=\"language-bash\">mkdir nodejs-multitenant-saas-api\ncd nodejs-multitenant-saas-api\nnpm init -y\nnpm install express pg jsonwebtoken bcryptjs express-rate-limit rate-limit-redis ioredis dotenv\nnpm install --save-dev jest supertest\n<\/code><\/pre>\n<h3 id=\"heading-starting-postgresql-and-redis-with-docker\">\u0688\u0627\u06a9\u0631 \u06a9\u06d2 \u0633\u0627\u062a\u06be PostgreSQL \u0627\u0648\u0631 Redis \u06a9\u06d2 \u0633\u0627\u062a\u06be \u0634\u0631\u0648\u0639\u0627\u062a \u06a9\u0631\u0646\u0627<\/h3>\n<p>\u0645\u0642\u0627\u0645\u06cc \u062a\u0646\u0635\u06cc\u0628 \u06a9\u0648 \u0686\u06be\u0648\u0691 \u062f\u06cc\u06ba\u06d4 \u0627\u06cc\u06a9 <code>docker-compose.yml<\/code> \u067e\u0631\u0648\u062c\u06cc\u06a9\u0679 \u0631\u0648\u0679 \u0633\u06d2 PostgreSQL \u0627\u0648\u0631 Redis \u062f\u0648\u0646\u0648\u06ba \u06a9\u0648 \u0644\u0648\u0688 \u06a9\u0631\u06cc\u06ba\u06d4<\/p>\n<pre><code class=\"language-yaml\">services:\n  postgres:\n    image: postgres:16-alpine\n    environment:\n      POSTGRES_DB: saas_api\n      POSTGRES_USER: app_user\n      POSTGRES_PASSWORD: app_password\n    ports:\n      - \"5432:5432\"\n    volumes:\n      - postgres_data:\/var\/lib\/postgresql\/data\n      - .\/schema.sql:\/docker-entrypoint-initdb.d\/01_schema.sql\n\n  redis:\n    image: redis:7-alpine\n    ports:\n      - \"6379:6379\"\n\nvolumes:\n  postgres_data:\n<\/code><\/pre>\n<p>\u06a9\u06c1 <code>schema.sql<\/code> \u062c\u0628 \u06a9\u0646\u0679\u06cc\u0646\u0631 \u067e\u06c1\u0644\u06cc \u0628\u0627\u0631 \u0634\u0631\u0648\u0639 \u06c1\u0648\u062a\u0627 \u06c1\u06d2 \u062a\u0648 \u0645\u0627\u0624\u0646\u0679 \u062e\u0648\u062f \u0628\u062e\u0648\u062f \u0627\u06cc\u0633 \u06a9\u06cc\u0648 \u0627\u06cc\u0644 \u0686\u0644\u0627\u062a\u0627 \u06c1\u06d2\u06d4 psql \u06a9\u06cc \u0636\u0631\u0648\u0631\u062a \u0646\u06c1\u06cc\u06ba\u06d4<\/p>\n<pre><code class=\"language-bash\">docker compose up -d\n<\/code><\/pre>\n<p><code>.env<\/code>    \u0645\u0646\u0635\u0648\u0628\u06d2 \u06a9\u06cc \u062c\u0691 \u0645\u06cc\u06ba:<\/p>\n<pre><code class=\"language-plaintext\">DATABASE_URL=postgresql:\/\/app_user:app_password@localhost:5432\/saas_api\nREDIS_URL=redis:\/\/localhost:6379\nJWT_SECRET=your_random_secret_here\nPORT=3000\nNODE_ENV=development\n<\/code><\/pre>\n<p>\u062f\u0631\u062c \u0630\u06cc\u0644 \u062f\u0631\u062c \u0646\u06c1 \u06a9\u0631\u06cc\u06ba: <code>JWT_SECRET<\/code> \u06c1\u0627\u062a\u06be \u0633\u06d2\u06d4 \u0686\u0644\u0627 \u06a9\u0631 \u0627\u06cc\u06a9 \u0628\u0646\u0627\u0626\u06cc\u06ba:<\/p>\n<pre><code class=\"language-bash\">node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"\n<\/code><\/pre>\n<p>\u0641\u0627\u0626\u0644 \u06a9\u06cc \u0633\u0627\u062e\u062a:<\/p>\n<pre><code class=\"language-plaintext\">nodejs-multitenant-saas-api\/\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 middleware\/\n\u2502   \u2502   \u251c\u2500\u2500 auth.js          # JWT verification + tenant extraction\n\u2502   \u2502   \u251c\u2500\u2500 rbac.js          # Role enforcement\n\u2502   \u2502   \u2514\u2500\u2500 rateLimiter.js   # Per-tenant rate limiting\n\u2502   \u251c\u2500\u2500 services\/\n\u2502   \u2502   \u2514\u2500\u2500 auditService.js  # Append-only audit logger\n\u2502   \u251c\u2500\u2500 repositories\/\n\u2502   \u2502   \u2514\u2500\u2500 projectRepo.js   # Tenant-safe DB queries\n\u2502   \u251c\u2500\u2500 routes\/\n\u2502   \u2502   \u2514\u2500\u2500 projects.js      # Route handlers\n\u2502   \u2514\u2500\u2500 utils\/\n\u2502       \u2514\u2500\u2500 token.js         # JWT token generation\n\u251c\u2500\u2500 db\/\n\u2502   \u251c\u2500\u2500 index.js             # PostgreSQL pool\n\u2502   \u2514\u2500\u2500 redis.js             # Redis client\n\u251c\u2500\u2500 docker-compose.yml\n\u251c\u2500\u2500 app.js\n\u251c\u2500\u2500 server.js\n\u2514\u2500\u2500 tests\/\n    \u2514\u2500\u2500 tenantIsolation.test.js\n<\/code><\/pre>\n<h3 id=\"heading-boilerplate-files\">\u0628\u0648\u0627\u0626\u0644\u0631 \u067e\u0644\u06cc\u0679 \u0641\u0627\u0626\u0644<\/h3>\n<p>\u0686\u0627\u0631 \u0641\u0627\u0626\u0644\u06cc\u06ba \u06c1\u06cc\u06ba \u062c\u0646 \u06a9\u0627 \u0679\u06cc\u0648\u0679\u0648\u0631\u06cc\u0644 \u0645\u06cc\u06ba \u062a\u0641\u0635\u06cc\u0644 \u0633\u06d2 \u0627\u062d\u0627\u0637\u06c1 \u0646\u06c1\u06cc\u06ba \u06a9\u06cc\u0627 \u06af\u06cc\u0627 \u06c1\u06d2\u060c \u0644\u06cc\u06a9\u0646 \u0622\u067e \u06a9\u0648 \u0679\u06cc\u0633\u0679 \u0641\u0627\u0626\u0644\u0648\u06ba \u06a9\u0648 \u0686\u0644\u0627\u0646\u06d2 \u06a9\u06d2 \u0644\u06cc\u06d2 \u0627\u0646 \u0686\u0627\u0631\u0648\u06ba \u06a9\u06cc \u0636\u0631\u0648\u0631\u062a \u06c1\u0648\u06af\u06cc\u06d4<\/p>\n<pre><code class=\"language-javascript\">\/\/ db\/index.js\nconst { Pool } = require('pg');\n\nconst pool = new Pool({ connectionString: process.env.DATABASE_URL });\n\npool.on('error', (err) => console.error('PostgreSQL error:', err.message));\n\nmodule.exports = { pool };\n<\/code><\/pre>\n<pre><code class=\"language-javascript\">\/\/ db\/redis.js\nconst Redis = require('ioredis');\n\nconst redisClient = new Redis(process.env.REDIS_URL);\n\nredisClient.on('error', (err) => console.error('Redis error:', err.message));\n\nmodule.exports = { redisClient };\n<\/code><\/pre>\n<pre><code class=\"language-javascript\">\/\/ app.js\nrequire('dotenv').config();\nconst express = require('express');\nconst projectsRouter = require('.\/src\/routes\/projects');\n\nconst app = express();\napp.use(express.json());\n\napp.use('\/api\/projects', projectsRouter);\n\n\/\/ Global error handler \u2014 must have 4 parameters to be recognised by Express\napp.use((err, req, res, next) => {\n  console.error(err.stack);\n  res.status(500).json({ error: 'Internal server error' });\n});\n\nmodule.exports = app;\n<\/code><\/pre>\n<pre><code class=\"language-javascript\">\/\/ server.js\nconst app = require('.\/app');\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => console.log(`Server running on port ${PORT}`));\n<\/code><\/pre>\n<p><code>bcryptjs<\/code>    \u0644\u0627\u06af \u0627\u0646 \u0627\u06cc\u0646\u0688 \u067e\u0648\u0627\u0626\u0646\u0679 \u0645\u06cc\u06ba \u0645\u0646\u0627\u0633\u0628 \u067e\u0627\u0633 \u0648\u0631\u0688 \u06c1\u06cc\u0634\u0646\u06af \u06a9\u06d2 \u0633\u0627\u062a\u06be \u0634\u0627\u0645\u0644 \u06c1\u06d2\u06d4 \u0645\u06cc\u06ba \u06cc\u06c1\u0627\u06ba \u0627\u0633 \u062d\u0635\u06d2 \u06a9\u0627 \u0627\u062d\u0627\u0637\u06c1 \u0646\u06c1\u06cc\u06ba \u06a9\u0631\u0648\u06ba \u06af\u0627\u060c \u0644\u06cc\u06a9\u0646 GitHub \u0630\u062e\u06cc\u0631\u06c1 \u0645\u06cc\u06ba \u0627\u06cc\u06a9 \u06a9\u0627\u0645 \u06a9\u0631\u0646\u06d2 \u0648\u0627\u0644\u0627 \u062d\u0635\u06c1 \u06c1\u06d2\u06d4 <code>\/api\/auth\/login<\/code> \u06c1\u0627\u06ba<\/p>\n<h2 id=\"heading-jwt-design-for-multi-tenancy\">\u06a9\u062b\u06cc\u0631 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631\u06cc \u06a9\u06d2 \u0644\u06cc\u06d2 JWT \u0688\u06cc\u0632\u0627\u0626\u0646<\/h2>\n<p>\u062f\u0648\u0646\u0648\u06ba <code>tenantId<\/code> \u0627\u0648\u0631 <code>role<\/code> JWT \u067e\u06d2 \u0644\u0648\u0688 \u067e\u0631 \u062c\u0627\u0626\u06cc\u06ba\u06d4 \u0646\u06cc\u0686\u06d2 \u06a9\u06cc \u06c1\u0631 \u0686\u06cc\u0632 \u0627\u0646 \u062f\u0648 \u0634\u0639\u0628\u0648\u06ba \u0633\u06d2 \u067e\u0691\u06be\u062a\u06cc \u06c1\u06d2\u06d4 \u0627\u06af\u0631 \u0622\u067e \u0627\u0633\u06d2 \u063a\u0644\u0637 \u0637\u0631\u06cc\u0642\u06d2 \u0633\u06d2 \u062f\u0631\u062c \u06a9\u0631\u062a\u06d2 \u06c1\u06cc\u06ba\u060c \u062a\u0648 \u06a9\u0686\u06be \u0628\u06be\u06cc \u0635\u062d\u06cc\u062d \u0637\u0631\u06cc\u0642\u06d2 \u0633\u06d2 \u06a9\u0627\u0645 \u0646\u06c1\u06cc\u06ba \u06a9\u0631\u06d2 \u06af\u0627\u06d4<\/p>\n<pre><code class=\"language-javascript\">\/\/ Example JWT payload\n{\n  \"userId\": \"usr_abc123\",\n  \"tenantId\": \"ten_xyz789\",\n  \"email\": \"alice@acme.com\",\n  \"role\": \"TenantAdmin\",\n  \"iat\": 1720000000,\n  \"exp\": 1720086400\n}\n<\/code><\/pre>\n<p>\u0627\u062c\u0627\u0632\u062a\u0648\u06ba \u06a9\u06cc \u062a\u0631\u062a\u06cc\u0628 \u0645\u06cc\u06ba \u06a9\u0631\u062f\u0627\u0631:<\/p>\n<ul>\n<li>\n<p><strong>\u0633\u067e\u0631 \u0627\u06cc\u0688\u0645\u0646\u0633\u0679\u0631\u06cc\u0679\u0631:<\/strong> \u0635\u0631\u0641 \u0627\u0646\u062f\u0631\u0648\u0646\u06cc \u0679\u06cc\u0645\u0648\u06ba \u06a9\u06d2 \u0644\u06cc\u06d2 \u06a9\u0631\u0627\u0633 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u062a\u06a9 \u0631\u0633\u0627\u0626\u06cc<\/p>\n<\/li>\n<li>\n<p><strong>\u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0627\u06cc\u0688\u0645\u0646\u0633\u0679\u0631\u06cc\u0679\u0631:<\/strong> \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06d2 \u0627\u0646\u062f\u0631 \u0645\u06a9\u0645\u0644 \u0631\u0633\u0627\u0626\u06cc<\/p>\n<\/li>\n<li>\n<p><strong>\u0631\u06a9\u0646:<\/strong> \u0627\u067e\u0646\u06d2 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06d2 \u0627\u0646\u062f\u0631 \u067e\u0691\u06be\u06cc\u06ba \u0627\u0648\u0631 \u0644\u06a9\u06be\u06cc\u06ba\u06d4<\/p>\n<\/li>\n<li>\n<p><strong>\u0646\u0627\u0638\u0631\u06cc\u0646:<\/strong> \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06d2 \u0627\u0646\u062f\u0631 \u0635\u0631\u0641 \u067e\u0691\u06be\u0646\u06d2 \u06a9\u06d2 \u0644\u06cc\u06d2<\/p>\n<\/li>\n<\/ul>\n<p>\u0627\u06cc\u06a9 \u0679\u0648\u06a9\u0646 \u0628\u0646\u0627\u0626\u06cc\u06ba (\u0679\u06cc\u0633\u0679\u0646\u06af \u0627\u0648\u0631 \u062a\u0635\u062f\u06cc\u0642 \u06a9\u06d2 \u0627\u062e\u062a\u062a\u0627\u0645\u06cc \u067e\u0648\u0627\u0626\u0646\u0679\u0633 \u06a9\u06d2 \u0644\u06cc\u06d2 \u0627\u0633\u062a\u0639\u0645\u0627\u0644 \u06a9\u06cc\u0627 \u062c\u0627\u062a\u0627 \u06c1\u06d2):<\/p>\n<pre><code class=\"language-javascript\">\/\/ src\/utils\/token.js\nconst jwt = require('jsonwebtoken');\n\nfunction generateToken({ userId, tenantId, email, role }) {\n  return jwt.sign(\n    { userId, tenantId, email, role },\n    process.env.JWT_SECRET,\n    { expiresIn: '24h' }\n  );\n}\n\nmodule.exports = { generateToken };\n<\/code><\/pre>\n<h2 id=\"heading-auth-and-rbac-middleware\">\u062a\u0648\u062b\u06cc\u0642 \u0627\u0648\u0631 RBAC \u0645\u0688\u0644 \u0648\u06cc\u0626\u0631<\/h2>\n<p>\u062a\u0648\u062b\u06cc\u0642 \u0645\u0688\u0644 \u0648\u06cc\u0626\u0631 \u062f\u0648 \u06a9\u0627\u0645 \u06a9\u0631\u062a\u0627 \u06c1\u06d2: JWT \u062f\u0633\u062a\u062e\u0637 \u06a9\u06cc \u062a\u0635\u062f\u06cc\u0642 \u06a9\u0631\u06cc\u06ba \u0627\u0648\u0631 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0633\u06cc\u0627\u0642 \u0648 \u0633\u0628\u0627\u0642 \u062d\u0627\u0635\u0644 \u06a9\u0631\u06cc\u06ba\u06d4 <code>req.user<\/code>.<\/p>\n<p>\u062f\u0648\u0633\u0631\u0627 \u062d\u0635\u06c1 \u0648\u06c1 \u06c1\u06d2 \u062c\u0633 \u067e\u0631 \u067e\u0648\u0631\u0627 \u0646\u0638\u0627\u0645 \u0645\u0646\u062d\u0635\u0631 \u06c1\u06d2\u06d4 \u062a\u0645\u0627\u0645 \u0633\u0648\u0627\u0644\u0627\u062a \u0646\u06cc\u0686\u06d2 \u06a9\u06cc \u0637\u0631\u0641 \u067e\u0691\u06be\u06cc\u06ba <code>req.user.tenantId<\/code>. \u06a9\u0644\u0627\u0626\u0646\u0679 \u06a9\u0648 \u0627\u0633 \u0628\u0627\u062a \u06a9\u0627 \u06a9\u0648\u0626\u06cc \u0645\u0637\u0644\u0628 \u0646\u06c1\u06cc\u06ba \u06c1\u06d2 \u06a9\u06c1 \u0648\u06c1 \u0642\u06cc\u0645\u062a \u06a9\u06cc\u0627 \u06c1\u06d2\u06d4 \u0633\u0631\u0648\u0631 \u0622\u067e \u06a9\u0648 \u0627\u06cc\u06a9 \u062f\u0633\u062a\u062e\u0637 \u0634\u062f\u06c1 \u0679\u0648\u06a9\u0646 \u0628\u06be\u06cc\u062c\u062a\u0627 \u06c1\u06d2\u060c \u0627\u0648\u0631 \u0633\u0631\u0648\u0631 \u0648\u0627\u067e\u0633 \u067e\u0691\u06be\u062a\u0627 \u06c1\u06d2 \u062c\u0648 \u0622\u067e \u0646\u06d2 \u062f\u0631\u062c \u06a9\u06cc\u0627 \u06c1\u06d2\u06d4<\/p>\n<pre><code class=\"language-javascript\">\/\/ src\/middleware\/auth.js\nconst jwt = require('jsonwebtoken');\n\nfunction authMiddleware(req, res, next) {\n  const authHeader = req.headers.authorization;\n  if (!authHeader?.startsWith('Bearer ')) {\n    return res.status(401).json({ error: 'Missing or malformed Authorization header' });\n  }\n\n  const token = authHeader.split(' ')[1];\n\n  try {\n    const decoded = jwt.verify(token, process.env.JWT_SECRET);\n\n    \/\/ tenantId always comes from the verified token \u2014 never req.body or req.params\n    req.user = {\n      userId:   decoded.userId,\n      tenantId: decoded.tenantId,\n      email:    decoded.email,\n      role:     decoded.role,\n    };\n\n    next();\n  } catch (err) {\n    return res.status(401).json({ error: 'Invalid or expired token' });\n  }\n}\n\nmodule.exports = { authMiddleware };\n<\/code><\/pre>\n<p>RBAC \u0645\u0688\u0644 \u0648\u06cc\u0626\u0631 \u0688\u06cc\u0632\u0627\u0626\u0646 \u06a9\u06d2 \u0644\u062d\u0627\u0638 \u0633\u06d2 \u062a\u0635\u062f\u06cc\u0642 \u0633\u06d2 \u0627\u0644\u06af \u06c1\u06d2\u06d4 \u062a\u0635\u062f\u06cc\u0642 \u062a\u0645\u0627\u0645 \u0631\u0627\u0633\u062a\u0648\u06ba \u067e\u0631 \u0686\u0644\u062a\u06cc \u06c1\u06d2\u06d4 \u0631\u0648\u0644 \u0627\u0646\u0641\u0648\u0631\u0633\u0645\u0646\u0679 \u06a9\u0627 \u0627\u0637\u0644\u0627\u0642 \u0635\u0631\u0641 \u0627\u0633 \u0648\u0642\u062a \u06c1\u0648\u062a\u0627 \u06c1\u06d2 \u062c\u0628 \u06a9\u0645 \u0627\u0632 \u06a9\u0645 \u0631\u0648\u0644\u0632 \u06a9\u06cc \u0636\u0631\u0648\u0631\u062a \u06c1\u0648\u06d4 \u0627\u062c\u0627\u0632\u062a \u06cc\u0627\u0641\u062a\u06c1 \u06a9\u0631\u062f\u0627\u0631\u0648\u06ba \u06a9\u0648 \u0645\u0646\u062a\u0642\u0644 \u06a9\u0631\u06cc\u06ba: <code>requireRole()<\/code> \u062f\u0631\u062c\u06c1 \u0628\u0646\u062f\u06cc \u06a9\u06d2 \u0633\u0627\u062a\u06be \u0635\u0627\u0631\u0641 \u06a9\u06cc \u0633\u0637\u062d \u06a9\u0627 \u0645\u0648\u0627\u0632\u0646\u06c1 \u06a9\u0631\u06cc\u06ba\u06d4 \u06a9\u0686\u06be \u062d\u0630\u0641 \u06a9\u0631\u0646\u06d2 \u06a9\u06cc \u06a9\u0648\u0634\u0634 \u06a9\u0631\u0646\u06d2 \u0648\u0627\u0644\u06d2 \u0646\u0627\u0638\u0631\u06cc\u0646 \u06c1\u06cc\u0646\u0688\u0644\u0631 \u06a9\u06d2 \u0686\u0644\u0646\u06d2 \u0633\u06d2 \u067e\u06c1\u0644\u06d2 403 \u062a\u06a9 \u067e\u06c1\u0646\u0686 \u062c\u0627\u0626\u06cc\u06ba \u06af\u06d2\u06d4<\/p>\n<pre><code class=\"language-javascript\">\/\/ src\/middleware\/rbac.js\nconst ROLE_HIERARCHY = {\n  SuperAdmin:   4,\n  TenantAdmin:  3,\n  Member:       2,\n  Viewer:       1,\n};\n\n\/\/ requireRole('TenantAdmin') \u2014 user must be TenantAdmin or higher\nfunction requireRole(...roles) {\n  return (req, res, next) => {\n    const userLevel = ROLE_HIERARCHY[req.user?.role] ?? 0;\n    const requiredLevel = Math.min(...roles.map(r => ROLE_HIERARCHY[r] ?? 999));\n\n    if (userLevel < requiredLevel) {\n      return res.status(403).json({\n        error: 'Insufficient permissions',\n        required: roles,\n        current: req.user?.role,\n      });\n    }\n\n    next();\n  };\n}\n\nmodule.exports = { requireRole };\n<\/code><\/pre>\n<h2 id=\"heading-the-tenant-safe-repository-layer\">\u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06d2 \u0644\u06cc\u06d2 \u0645\u062d\u0641\u0648\u0638 \u0630\u062e\u06cc\u0631\u06c1 \u06a9\u06cc \u067e\u0631\u062a<\/h2>\n<p>\u062a\u0646\u06c1\u0627\u0626\u06cc \u06cc\u06c1\u0627\u06ba \u0631\u06c1\u062a\u06cc \u06c1\u06d2\u06d4 \u062a\u0645\u0627\u0645 \u062e\u0635\u0648\u0635\u06cc\u0627\u062a <code>tenantId<\/code> \u0633\u06d2 \u0644\u06cc\u06d2 \u06af\u0626\u06d2 \u0645\u0637\u0644\u0648\u0628\u06c1 \u062f\u0644\u0627\u0626\u0644 <code>req.user<\/code> \u06c1\u06cc\u0646\u0688\u0644\u0631 \u06a9\u06cc \u0637\u0631\u0641 \u0633\u06d2. \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06cc \u06af\u0646\u062c\u0627\u0626\u0634 \u0641\u0631\u0627\u06c1\u0645 \u06a9\u06cc\u06d2 \u0628\u063a\u06cc\u0631 \u0627\u0633\u06d2 \u06a9\u0627\u0644 \u06a9\u0631\u0646\u06d2 \u06a9\u0627 \u06a9\u0648\u0626\u06cc \u0637\u0631\u06cc\u0642\u06c1 \u0646\u06c1\u06cc\u06ba \u06c1\u06d2\u06d4 \u0645\u06cc\u06ba \u0646\u06d2 \u0679\u06cc\u0645\u0648\u06ba \u06a9\u0648 URL \u067e\u06cc\u0631\u0627\u0645\u06cc\u0679\u0631\u0632 \u06a9\u0627 \u0627\u0633\u062a\u0639\u0645\u0627\u0644 \u06a9\u0631\u062a\u06d2 \u06c1\u0648\u0626\u06d2 \u0627\u0633\u06d2 \u0633\u0646\u0628\u06be\u0627\u0644\u0646\u06d2 \u06a9\u06cc \u06a9\u0648\u0634\u0634 \u06a9\u0631\u062a\u06d2 \u062f\u06cc\u06a9\u06be\u0627 \u06c1\u06d2 (<code>GET \/api\/projects?tenantId=xyz<\/code>) \u0627\u0648\u0631 \u0627\u0633\u06d2 \u0627\u0644\u06af \u062a\u06be\u0644\u06af \u06a9\u06c1\u062a\u06d2 \u06c1\u06cc\u06ba\u06d4 \u06cc\u06c1 \u0633\u0686 \u0646\u06c1\u06cc\u06ba \u06c1\u06d2\u06d4 \u06c1\u0631 \u06a9\u0644\u0627\u0626\u0646\u0679 \u0627\u067e\u0646\u06d2 \u0627\u0633\u062a\u0641\u0633\u0627\u0631 \u06a9\u06d2 \u0633\u0644\u0633\u0644\u06d2 \u0645\u06cc\u06ba \u062c\u0648 \u0686\u0627\u06c1\u06d2 \u0628\u06be\u06cc\u062c\u062a\u0627 \u06c1\u06d2\u06d4<\/p>\n<pre><code class=\"language-javascript\">\/\/ src\/repositories\/projectRepo.js\nconst { pool } = require('..\/..\/db');\n\n\/\/ List all projects for a tenant \u2014 tenantId is ALWAYS from the JWT\nasync function listProjects(tenantId) {\n  const result = await pool.query(\n    `SELECT id, name, description, created_by, created_at\n     FROM projects\n     WHERE tenant_id = $1\n     ORDER BY created_at DESC`,\n    [tenantId]\n  );\n  return result.rows;\n}\n\n\/\/ Get a single project \u2014 returns null if it belongs to a different tenant\n\/\/ NOTE: Returns 404 (not 403) intentionally \u2014 don't reveal the resource exists\nasync function getProject(id, tenantId) {\n  const result = await pool.query(\n    `SELECT id, name, description, created_by, created_at\n     FROM projects\n     WHERE id = $1 AND tenant_id = $2`,\n    [id, tenantId]\n  );\n  return result.rows[0] || null;\n}\n\nasync function createProject({ tenantId, name, description, createdBy }) {\n  const result = await pool.query(\n    `INSERT INTO projects (tenant_id, name, description, created_by)\n     VALUES ($1, $2, $3, $4)\n     RETURNING *`,\n    [tenantId, name, description, createdBy]\n  );\n  return result.rows[0];\n}\n\nasync function updateProject(id, tenantId, updates) {\n  const result = await pool.query(\n    `UPDATE projects\n     SET name = COALESCE($3, name),\n         description = COALESCE($4, description),\n         updated_at = NOW()\n     WHERE id = $1 AND tenant_id = $2\n     RETURNING *`,\n    [id, tenantId, updates.name, updates.description]\n  );\n  return result.rows[0] || null;\n}\n\nasync function deleteProject(id, tenantId) {\n  const result = await pool.query(\n    `DELETE FROM projects WHERE id = $1 AND tenant_id = $2 RETURNING id`,\n    [id, tenantId]\n  );\n  return result.rows[0] || null;\n}\n\nmodule.exports = { listProjects, getProject, createProject, updateProject, deleteProject };\n<\/code><\/pre>\n<p>\u06a9\u0633 \u0686\u06cc\u0632 \u067e\u0631 \u062a\u0648\u062c\u06c1 \u062f\u06cc\u06ba\u06d4 <code>getProject<\/code> \u0627\u0633 \u0648\u0642\u062a \u0627\u0646\u062c\u0627\u0645 \u062f\u06cc\u0627 \u062c\u0627\u062a\u0627 \u06c1\u06d2 \u062c\u0628 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 A \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 B \u06a9\u06d2 \u0648\u0633\u0627\u0626\u0644 \u062d\u0627\u0635\u0644 \u06a9\u0631\u0646\u06d2 \u06a9\u06cc \u06a9\u0648\u0634\u0634 \u06a9\u0631\u062a\u0627 \u06c1\u06d2\u06d4 \u0627\u0633\u062a\u0641\u0633\u0627\u0631 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0627\u06d2 \u06a9\u06d2 \u0633\u0627\u062a\u06be \u0686\u0644\u062a\u0627 \u06c1\u06d2\u06d4 <code>tenantId<\/code>. \u062d\u0627\u0644\u062a <code>id = $1 AND tenant_id = $2<\/code> \u06a9\u0686\u06be \u0628\u06be\u06cc \u0646\u06c1\u06cc\u06ba \u0645\u0644\u062a\u0627\u06d4 <code>null<\/code> \u0648\u0627\u067e\u0633 \u0622\u0624 \u0627\u0648\u0631 \u06c1\u06cc\u0646\u0688\u0644\u0631 <code>404<\/code>. \u0646\u06c1\u06cc\u06ba <code>403<\/code>. 403 \u06a9\u0627\u0644 \u06a9\u0631\u0646\u06d2 \u0648\u0627\u0644\u06d2 \u06a9\u0648 \u0628\u062a\u0627\u062a\u0627 \u06c1\u06d2 \u06a9\u06c1 \u0648\u0633\u06cc\u0644\u06c1 \u0645\u0648\u062c\u0648\u062f \u06c1\u06d2 \u0644\u06cc\u06a9\u0646 \u0627\u0633 \u062a\u06a9 \u0631\u0633\u0627\u0626\u06cc \u062d\u0627\u0635\u0644 \u0646\u06c1\u06cc\u06ba \u06a9\u06cc \u062c\u0627 \u0633\u06a9\u062a\u06cc\u060c \u06cc\u06c1 \u0648\u06c1 \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u06c1\u06d2 \u062c\u0648 \u0627\u0646 \u06a9\u06d2 \u067e\u0627\u0633 \u0646\u06c1\u06cc\u06ba \u06c1\u0648\u0646\u06cc \u0686\u0627\u06c1\u06cc\u06d2\u06d4<\/p>\n<h2 id=\"heading-audit-logging-service\">\u0622\u0688\u0679 \u0644\u0627\u06af\u0646\u06af \u0633\u0631\u0648\u0633<\/h2>\n<pre><code class=\"language-javascript\">\/\/ src\/services\/auditService.js\nconst { pool } = require('..\/..\/db');\n\nasync function log({\n  tenantId,\n  userId,\n  userEmail,\n  userRole,          \/\/ role at time of action \u2014 roles change, log should not\n  action,            \/\/ 'CREATE' | 'UPDATE' | 'DELETE' | 'VIEW'\n  resource,          \/\/ table name\n  resourceId = null,\n  oldValues = null,\n  newValues = null,\n  ipAddress = null,\n  userAgent = null,\n}) {\n  const query = `\n    INSERT INTO audit_logs\n      (tenant_id, user_id, user_email, user_role, action, resource,\n       resource_id, old_values, new_values, ip_address, user_agent)\n    VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)\n  `;\n\n  const values = [\n    tenantId, userId, userEmail, userRole, action, resource,\n    resourceId,\n    oldValues  ? JSON.stringify(oldValues)  : null,\n    newValues  ? JSON.stringify(newValues)  : null,\n    ipAddress,\n    userAgent,\n  ];\n\n  \/\/ Fire-and-forget \u2014 audit logging must never block or fail a user request\n  pool.query(query, values).catch((err) => {\n    console.error('[AuditService] Failed to write log:', err.message);\n  });\n}\n\nmodule.exports = { log };\n<\/code><\/pre>\n<p>\u0642\u0628\u0636\u06c1 <code>userRole<\/code> \u0644\u06a9\u06be\u0646\u06d2 \u06a9\u0627 \u0648\u0642\u062a \u062c\u062a\u0646\u0627 \u0644\u06af\u062a\u0627 \u06c1\u06d2 \u0627\u0633 \u0633\u06d2 \u0632\u06cc\u0627\u062f\u06c1 \u0627\u06c1\u0645 \u06c1\u06d2\u06d4 \u062d\u0642\u06cc\u0642\u062a \u06a9\u06d2 \u0628\u0639\u062f \u0635\u0627\u0631\u0641 \u06a9\u06d2 \u06a9\u0631\u062f\u0627\u0631 \u0628\u062f\u0644 \u062c\u0627\u062a\u06d2 \u06c1\u06cc\u06ba\u06d4 \u06a9\u0633\u06cc \u06a9\u06cc \u062a\u0646\u0632\u0644\u06cc \u0627\u0648\u0631 \u0645\u0631\u0627\u0639\u0627\u062a \u0645\u0646\u0633\u0648\u062e \u06c1\u0648 \u062c\u0627\u062a\u06cc \u06c1\u06cc\u06ba\u06d4 \u0627\u06af\u0631 \u0644\u0627\u06af \u0645\u06cc\u06ba \u0635\u0631\u0641 \u06cc\u0648\u0632\u0631 \u0622\u0626\u06cc \u0688\u06cc \u06c1\u06cc \u0631\u06cc\u06a9\u0627\u0631\u0688 \u06a9\u06cc \u06af\u0626\u06cc \u06c1\u06d2\u060c \u062a\u0648 \u0627\u0633 \u0628\u0627\u062a \u06a9\u0627 \u0633\u06cc\u0627\u0642 \u0648 \u0633\u0628\u0627\u0642 \u062e\u062a\u0645 \u06c1\u0648 \u062c\u0627\u0626\u06d2 \u06af\u0627 \u06a9\u06c1 \u06a9\u0627\u0631\u0631\u0648\u0627\u0626\u06cc \u06a9\u06d2 \u0648\u0642\u062a \u0635\u0627\u0631\u0641 \u06a9\u06d2 \u067e\u0627\u0633 \u06a9\u0648\u0646 \u0633\u06cc \u0627\u062c\u0627\u0632\u062a\u06cc\u06ba \u062a\u06be\u06cc\u06ba\u06d4 \u0627\u067e\u0646\u06d2 \u06a9\u0631\u062f\u0627\u0631 \u06a9\u0648 \u0645\u062d\u0641\u0648\u0638 \u06a9\u0631\u06cc\u06ba \u062a\u0627\u06a9\u06c1 \u0622\u067e \u06a9\u0648 \u06c1\u0645\u06cc\u0634\u06c1 \u0645\u0639\u0644\u0648\u0645 \u06c1\u0648 \u06a9\u06c1 \u0622\u067e \u06a9\u0633 \u06a9\u0631\u062f\u0627\u0631 \u0645\u06cc\u06ba \u06a9\u0627\u0645 \u06a9\u0631 \u0631\u06c1\u06d2 \u062a\u06be\u06d2\u06d4<\/p>\n<h2 id=\"heading-per-tenant-rate-limiting\">\u0641\u06cc \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0634\u0631\u062d \u06a9\u06cc \u062d\u062f<\/h2>\n<p>SaaS \u0645\u06cc\u06ba IP \u067e\u0631 \u0645\u0628\u0646\u06cc \u0634\u0631\u062d \u06a9\u0648 \u0645\u062d\u062f\u0648\u062f \u06a9\u0631\u0646\u0627 \u0628\u0646\u062f \u06a9\u0631 \u062f\u06cc\u0627 \u06af\u06cc\u0627 \u06c1\u06d2\u06d4 \u0627\u0646\u0679\u0631\u067e\u0631\u0627\u0626\u0632 \u06a9\u06d2 \u0635\u0627\u0631\u0641\u06cc\u0646 \u0627\u06cc\u06a9 \u06c1\u06cc NAT \u06af\u06cc\u0679 \u0648\u06d2 \u06a9\u06d2 \u0630\u0631\u06cc\u0639\u06d2 \u0633\u06cc\u0646\u06a9\u0691\u0648\u06ba \u0635\u0627\u0631\u0641\u06cc\u0646 \u06a9\u0648 \u0631\u0648\u0679 \u06a9\u0631 \u06a9\u06d2 \u0627\u06cc\u06a9 IP \u0627\u06cc\u0688\u0631\u06cc\u0633 \u06a9\u0627 \u0627\u0634\u062a\u0631\u0627\u06a9 \u06a9\u0631 \u0633\u06a9\u062a\u06d2 \u06c1\u06cc\u06ba\u06d4 \u0627\u06cc\u06a9 \u0628\u06be\u0627\u0631\u06cc \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0627\u0633 \u067e\u062a\u06d2 \u067e\u0631 \u0628\u0627\u0642\u06cc \u0633\u0628 \u06a9\u0648 \u0645\u062d\u062f\u0648\u062f \u06a9\u0631\u062a\u0627 \u06c1\u06d2\u06d4<\/p>\n<p>\u0645\u06cc\u06ba \u0646\u06d2 \u0679\u06cc\u0645\u0648\u06ba \u06a9\u0648 \u0627\u0633 \u0645\u0634\u06a9\u0644 \u0637\u0631\u06cc\u0642\u06d2 \u0633\u06d2 \u062f\u0631\u06cc\u0627\u0641\u062a \u06a9\u0631\u062a\u06d2 \u06c1\u0648\u0626\u06d2 \u062f\u06cc\u06a9\u06be\u0627 \u06c1\u06d2 \u062c\u0628 \u0627\u06cc\u06a9 \u0627\u0646\u0679\u0631\u067e\u0631\u0627\u0626\u0632 \u06af\u0627\u06c1\u06a9 \u0627\u0686\u0627\u0646\u06a9 \u0627\u067e\u0646\u06d2 API \u0645\u06cc\u06ba \u0633\u06cc\u0644\u0627\u0628 \u0622 \u062c\u0627\u062a\u0627 \u06c1\u06d2 \u0627\u0648\u0631 \u062f\u0648\u0633\u0631\u06d2 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u0628\u063a\u06cc\u0631 \u06a9\u0633\u06cc \u0648\u0636\u0627\u062d\u062a \u06a9\u06d2 429 \u0648\u0635\u0648\u0644 \u06a9\u0631\u0646\u0627 \u0634\u0631\u0648\u0639 \u06a9\u0631 \u062f\u06cc\u062a\u06d2 \u06c1\u06cc\u06ba\u06d4 \u0631\u06cc\u0646\u062c \u06a9\u06cc \u067e\u0627\u0628\u0646\u062f\u06cc\u0627\u06ba: <code>tenant_id<\/code> \u0627\u0633 \u06a9\u06d2 \u0628\u062c\u0627\u0626\u06d2\u06d4<\/p>\n<pre><code class=\"language-javascript\">\/\/ src\/middleware\/rateLimiter.js\nconst rateLimit = require('express-rate-limit');\nconst { RedisStore } = require('rate-limit-redis');\nconst { redisClient } = require('..\/..\/db\/redis');\n\n\/\/ Rate limits by plan \u2014 extend as needed\nconst PLAN_LIMITS = {\n  free:       { max: 100,  windowMs: 15 * 60 * 1000 }, \/\/ 100 req \/ 15 min\n  pro:        { max: 500,  windowMs: 15 * 60 * 1000 }, \/\/ 500 req \/ 15 min\n  enterprise: { max: 2000, windowMs: 15 * 60 * 1000 }, \/\/ 2000 req \/ 15 min\n};\n\nfunction createTenantRateLimiter(plan = 'free') {\n  const limits = PLAN_LIMITS[plan] || PLAN_LIMITS.free;\n\n  return rateLimit({\n    windowMs: limits.windowMs,\n    max: limits.max,\n    \/\/ Key = tenant_id from verified JWT \u2014 NOT the IP address\n    keyGenerator: (req) => `tenant:${req.user?.tenantId || req.ip}`,\n    store: new RedisStore({\n      sendCommand: (...args) => redisClient.call(...args),\n    }),\n    handler: (req, res) => {\n      res.status(429).json({\n        error: 'Too many requests',\n        retryAfter: Math.ceil(limits.windowMs \/ 1000),\n      });\n    },\n  });\n}\n\n\/\/ Default limiter for all API routes\nconst defaultLimiter = createTenantRateLimiter('free');\n\nmodule.exports = { defaultLimiter, createTenantRateLimiter };\n<\/code><\/pre>\n<h2 id=\"heading-building-the-routes\">\u0631\u0627\u0633\u062a\u06d2 \u06a9\u06cc \u062a\u0639\u0645\u06cc\u0631<\/h2>\n<p>\u06cc\u06c1 \u0648\u06c1 \u062c\u06af\u06c1 \u06c1\u06d2 \u062c\u06c1\u0627\u06ba \u0633\u0628 \u06a9\u0686\u06be \u062c\u0648\u0691\u062a\u0627 \u06c1\u06d2\u06d4 \u062a\u0635\u062f\u06cc\u0642 \u0627\u0648\u0631 \u0634\u0631\u062d \u06a9\u06cc \u062d\u062f \u067e\u0648\u0631\u06d2 \u0631\u0648\u0679\u0631 \u067e\u0631 \u0644\u0627\u06af\u0648 \u06c1\u0648\u062a\u06cc \u06c1\u06d2\u06d4 \u06a9\u0631\u062f\u0627\u0631 \u06a9\u06cc \u062a\u0635\u062f\u06cc\u0642 \u0627\u0646\u0641\u0631\u0627\u062f\u06cc \u0631\u0627\u0633\u062a\u0648\u06ba \u067e\u0631 \u06c1\u0648\u062a\u06cc \u06c1\u06d2\u06d4 \u062c\u0628 \u0628\u06be\u06cc \u062a\u062d\u0631\u06cc\u0631 \u0645\u06a9\u0645\u0644 \u06c1\u0648\u062a\u06cc \u06c1\u06d2 \u0622\u0688\u0679 \u0644\u0627\u06af \u0686\u0644\u062a\u0627 \u06c1\u06d2\u06d4 <code>tenantId<\/code> \u06cc\u06c1 \u062f\u0631\u062e\u0648\u0627\u0633\u062a \u06a9\u06d2 \u062c\u0633\u0645 \u06cc\u0627 URL \u0633\u06d2 \u0646\u06c1\u06cc\u06ba \u0622\u062a\u0627 \u06c1\u06d2\u06d4 <code>req.user.tenantId<\/code> \u0622\u0633 \u067e\u0627\u0633 \u06a9\u06d2 \u06a9\u0648\u0626\u06cc \u0631\u0627\u0633\u062a\u06d2 \u0646\u06c1\u06cc\u06ba \u06c1\u06cc\u06ba \u06a9\u06cc\u0648\u0646\u06a9\u06c1 \u062a\u0635\u062f\u06cc\u0642 \u0634\u062f\u06c1 \u0679\u0648\u06a9\u0646 \u06a9\u06d2 \u0644\u06cc\u06d2 \u062a\u0635\u062f\u06cc\u0642 \u0645\u0688\u0644 \u0648\u06cc\u0626\u0631 \u06a9\u06d2 \u0630\u0631\u06cc\u0639\u06d2 \u0642\u0627\u0626\u0645 \u06a9\u0631\u062f\u06c1 \u06cc\u06c1 \u0648\u0627\u062d\u062f \u0630\u0631\u06cc\u0639\u06c1 \u06c1\u06d2\u06d4<\/p>\n<p>\u0627\u06cc\u06a9\u0633\u067e\u0631\u06cc\u0633 4 \u06a9\u06cc \u0639\u0645\u0644\u06cc \u062a\u0641\u0635\u06cc\u0644\u0627\u062a \u0645\u06cc\u06ba \u0633\u06d2 \u0627\u06cc\u06a9 \u06cc\u06c1 \u06c1\u06d2 \u06a9\u06c1 \u06cc\u06c1 \u062e\u0648\u062f \u0628\u062e\u0648\u062f \u063a\u06cc\u0631 \u0645\u0637\u0627\u0628\u0642\u062a \u067e\u0630\u06cc\u0631 \u063a\u0644\u0637\u06cc\u0648\u06ba \u06a9\u0648 \u0646\u06c1\u06cc\u06ba \u067e\u06a9\u0691\u062a\u0627 \u06c1\u06d2\u06d4 \u062a\u0645\u0627\u0645 \u06c1\u06cc\u0646\u0688\u0644\u0631\u0632 \u0627\u067e\u0646\u06cc \u0645\u0646\u0637\u0642 \u06a9\u0648 \u0627\u06cc\u06a9 \u06a9\u0648\u0634\u0634\/\u06a9\u06cc\u0686 \u0645\u06cc\u06ba \u0633\u0645\u06cc\u0679\u062a\u06d2 \u06c1\u06cc\u06ba \u0627\u0648\u0631 \u0646\u0627\u06a9\u0627\u0645\u06cc \u06a9\u0648 \u0627\u06af\u0644\u06d2 \u062a\u06a9 \u067e\u06c1\u0646\u0686\u0627 \u062f\u06cc\u062a\u06d2 \u06c1\u06cc\u06ba\u06d4 <code>next(err)<\/code>. \u0627\u0633 \u06a9\u0648 \u0686\u06be\u0648\u0691\u0646\u06d2 \u0633\u06d2 \u0627\u06cc\u06a9 \u062e\u0627\u0644\u06cc 500 \u0648\u0627\u067e\u0633 \u0622\u062c\u0627\u0626\u06d2 \u06af\u0627 \u062c\u0633 \u0645\u06cc\u06ba \u0644\u0627\u06af \u0627\u0646\u0679\u0631\u06cc\u0632 \u06cc\u0627 \u0622\u0688\u0679 \u0679\u0631\u06cc\u0644\u0632 \u06a9\u06d2 \u0628\u063a\u06cc\u0631 \u06c1\u06cc\u0646\u0688\u0644 \u06a9\u06cc\u06d2 \u06af\u0626\u06d2 \u0648\u0639\u062f\u06d2 \u06a9\u06d2 \u0645\u0633\u062a\u0631\u062f \u06c1\u0648\u0646\u06d2 \u06a9\u06cc \u0648\u062c\u06c1 \u0633\u06d2\u06d4 \u0631\u0648\u0679\u0631 \u06a9\u06d2 \u0627\u0648\u067e\u0631 \u062f\u06cc \u06af\u0626\u06cc \u062a\u0641\u0635\u06cc\u0644 \u06c1\u0645\u06cc\u06ba \u06cc\u0627\u062f \u062f\u0644\u0627\u062a\u06cc \u06c1\u06d2 \u06a9\u06c1 \u067e\u06cc\u0679\u0631\u0646 \u062c\u0627\u0646 \u0628\u0648\u062c\u06be \u06a9\u0631 \u0628\u0646\u0627\u06cc\u0627 \u06af\u06cc\u0627 \u06c1\u06d2\u06d4<\/p>\n<pre><code class=\"language-javascript\">\/\/ src\/routes\/projects.js\nconst express = require('express');\nconst { authMiddleware }  = require('..\/middleware\/auth');\nconst { requireRole }     = require('..\/middleware\/rbac');\nconst { defaultLimiter }  = require('..\/middleware\/rateLimiter');\nconst audit               = require('..\/services\/auditService');\nconst repo                = require('..\/repositories\/projectRepo');\n\nconst router = express.Router();\n\n\/\/ All routes require authentication\nrouter.use(authMiddleware);\nrouter.use(defaultLimiter);\n\n\/\/ Express 4 does not catch async errors automatically.\n\/\/ Every handler must wrap await calls in try\/catch and pass errors to next().\n\/\/ Without this, an unhandled promise rejection silently returns 500\n\/\/ with no useful message and no audit log entry.\n\n\/\/ GET \/api\/projects \u2014 list all (Viewer and above)\nrouter.get('\/', async (req, res, next) => {\n  try {\n    const projects = await repo.listProjects(req.user.tenantId);\n\n    audit.log({\n      tenantId:   req.user.tenantId,\n      userId:     req.user.userId,\n      userEmail:  req.user.email,\n      userRole:   req.user.role,\n      action:     'VIEW',\n      resource:   'projects',\n      ipAddress:  req.ip,\n      userAgent:  req.headers['user-agent'],\n    });\n\n    res.json(projects);\n  } catch (err) {\n    next(err);\n  }\n});\n\n\/\/ GET \/api\/projects\/:id \u2014 single project (Viewer and above)\nrouter.get('\/:id', async (req, res, next) => {\n  try {\n    const project = await repo.getProject(req.params.id, req.user.tenantId);\n    if (!project) return res.status(404).json({ error: 'Not found' });\n    res.json(project);\n  } catch (err) {\n    next(err);\n  }\n});\n\n\/\/ POST \/api\/projects \u2014 create (Member and above)\nrouter.post('\/', requireRole('Member', 'TenantAdmin', 'SuperAdmin'), async (req, res, next) => {\n  try {\n    const { name, description } = req.body;\n    if (!name) return res.status(400).json({ error: 'name is required' });\n\n    const project = await repo.createProject({\n      tenantId:    req.user.tenantId,\n      name,\n      description,\n      createdBy:   req.user.userId,\n    });\n\n    audit.log({\n      tenantId:    req.user.tenantId,\n      userId:      req.user.userId,\n      userEmail:   req.user.email,\n      userRole:    req.user.role,\n      action:      'CREATE',\n      resource:    'projects',\n      resourceId:  project.id,\n      newValues:   project,\n      ipAddress:   req.ip,\n      userAgent:   req.headers['user-agent'],\n    });\n\n    res.status(201).json(project);\n  } catch (err) {\n    next(err);\n  }\n});\n\n\/\/ PUT \/api\/projects\/:id \u2014 update (Member and above)\nrouter.put('\/:id', requireRole('Member', 'TenantAdmin', 'SuperAdmin'), async (req, res, next) => {\n  try {\n    const oldProject = await repo.getProject(req.params.id, req.user.tenantId);\n    if (!oldProject) return res.status(404).json({ error: 'Not found' });\n\n    const updated = await repo.updateProject(req.params.id, req.user.tenantId, req.body);\n\n    audit.log({\n      tenantId:    req.user.tenantId,\n      userId:      req.user.userId,\n      userEmail:   req.user.email,\n      userRole:    req.user.role,\n      action:      'UPDATE',\n      resource:    'projects',\n      resourceId:  req.params.id,\n      oldValues:   oldProject,\n      newValues:   updated,\n      ipAddress:   req.ip,\n      userAgent:   req.headers['user-agent'],\n    });\n\n    res.json(updated);\n  } catch (err) {\n    next(err);\n  }\n});\n\n\/\/ DELETE \/api\/projects\/:id \u2014 TenantAdmin and above only\nrouter.delete('\/:id', requireRole('TenantAdmin', 'SuperAdmin'), async (req, res, next) => {\n  try {\n    const project = await repo.getProject(req.params.id, req.user.tenantId);\n    if (!project) return res.status(404).json({ error: 'Not found' });\n\n    await repo.deleteProject(req.params.id, req.user.tenantId);\n\n    audit.log({\n      tenantId:    req.user.tenantId,\n      userId:      req.user.userId,\n      userEmail:   req.user.email,\n      userRole:    req.user.role,\n      action:      'DELETE',\n      resource:    'projects',\n      resourceId:  req.params.id,\n      oldValues:   project,\n      ipAddress:   req.ip,\n      userAgent:   req.headers['user-agent'],\n    });\n\n    res.json({ deleted: true });\n  } catch (err) {\n    next(err);\n  }\n});\n\nmodule.exports = router;\n<\/code><\/pre>\n<h2 id=\"heading-testing-tenant-isolation\">\u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u062a\u0646\u06c1\u0627\u0626\u06cc \u06a9\u06cc \u062c\u0627\u0646\u0686<\/h2>\n<p>\u0627\u06af\u0631 \u0622\u067e \u0642\u0631\u0646\u0637\u06cc\u0646\u06c1 \u0679\u06cc\u0633\u0679 \u0686\u06be\u0648\u0691 \u062f\u06cc\u062a\u06d2 \u06c1\u06cc\u06ba \u062a\u0648 \u0622\u067e \u0627\u0646\u062f\u06be\u06d2 \u06c1\u0648 \u062c\u0627\u0626\u06cc\u06ba \u06af\u06d2\u06d4 \u0627\u06cc\u067e\u0644\u06cc\u06a9\u06cc\u0634\u0646 \u0686\u0644\u062a\u06cc \u0631\u06c1\u062a\u06cc \u06c1\u06d2 \u0627\u0648\u0631 \u06a9\u0648\u0626\u06cc \u063a\u0644\u0637\u06cc \u0646\u06c1\u06cc\u06ba \u06c1\u0648\u062a\u06cc\u060c \u0644\u06cc\u06a9\u0646 \u062f\u0648\u0646\u0648\u06ba \u0635\u0627\u0631\u0641\u06cc\u0646 \u0627\u06cc\u06a9 \u062f\u0648\u0633\u0631\u06d2 \u06a9\u0627 \u0688\u06cc\u0679\u0627 \u067e\u0691\u06be \u0631\u06c1\u06d2 \u06c1\u06cc\u06ba\u06d4<\/p>\n<p>\u0645\u06cc\u06ba \u0646\u06d2 \u0627\u0633\u06d2 \u0645\u06c1\u06cc\u0646\u0648\u06ba \u062a\u06a9 \u067e\u0631\u0648\u0688\u06a9\u0634\u0646 \u0645\u06cc\u06ba \u0628\u06d2 \u067e\u062a\u06c1 \u06c1\u0648\u062a\u06d2 \u062f\u06cc\u06a9\u06be\u0627 \u06a9\u06cc\u0648\u0646\u06a9\u06c1 \u062d\u0642\u06cc\u0642\u062a \u0645\u06cc\u06ba \u06a9\u0686\u06be \u0628\u06be\u06cc \u0646\u06c1\u06cc\u06ba \u0679\u0648\u0679\u0627\u06d4 \u063a\u0644\u0637 \u0688\u06cc\u0679\u0627 \u062e\u0627\u0645\u0648\u0634\u06cc \u0633\u06d2 \u0638\u0627\u06c1\u0631 \u06c1\u0648\u0627\u06d4 \u062a\u0645\u0627\u0645 \u067e\u0644 \u062f\u0631\u062e\u0648\u0627\u0633\u062a\u0648\u06ba \u06a9\u06cc \u062e\u0648\u062f\u06a9\u0627\u0631 \u062c\u0627\u0646\u0686 \u0627\u0646\u06c1\u06cc\u06ba \u062c\u0644\u062f \u067e\u06a9\u0691\u0646\u06d2 \u06a9\u0627 \u0648\u0627\u062d\u062f \u0642\u0627\u0628\u0644 \u0627\u0639\u062a\u0645\u0627\u062f \u0637\u0631\u06cc\u0642\u06c1 \u06c1\u06d2\u06d4<\/p>\n<pre><code class=\"language-javascript\">\/\/ tests\/tenantIsolation.test.js\nrequire('dotenv').config();  \/\/ must be first \u2014 loads DATABASE_URL and REDIS_URL\nconst request = require('supertest');\nconst app     = require('..\/app');\nconst { generateToken } = require('..\/src\/utils\/token');\nconst { pool }        = require('..\/db');\nconst { redisClient } = require('..\/db\/redis');\n\n\/\/ Test fixture: two isolated tenants, one project in Tenant B\nasync function seedTestData() {\n  \/\/ Clean up from any previous run to avoid unique-constraint failures\n  await pool.query(`DELETE FROM projects WHERE name LIKE 'TEST-%'`);\n  await pool.query(`DELETE FROM tenants WHERE name IN ('Tenant A', 'Tenant B')`);\n\n  const tenantA = (await pool.query(\n    `INSERT INTO tenants (name, plan) VALUES ('Tenant A', 'pro') RETURNING id`\n  )).rows[0].id;\n\n  const tenantB = (await pool.query(\n    `INSERT INTO tenants (name, plan) VALUES ('Tenant B', 'pro') RETURNING id`\n  )).rows[0].id;\n\n  const userA = (await pool.query(\n    `INSERT INTO users (tenant_id, email, role) VALUES ($1, 'usera@a.com', 'Member') RETURNING id`,\n    [tenantA]\n  )).rows[0].id;\n\n  \/\/ userB owns the project in Tenant B \u2014 satisfies the created_by FK constraint\n  const userB = (await pool.query(\n    `INSERT INTO users (tenant_id, email, role) VALUES ($1, 'userb@b.com', 'Member') RETURNING id`,\n    [tenantB]\n  )).rows[0].id;\n\n  const projectB = (await pool.query(\n    `INSERT INTO projects (tenant_id, name, created_by)\n     VALUES ($1, 'TEST-Secret Project', $2) RETURNING id`,\n    [tenantB, userB]\n  )).rows[0].id;\n\n  return { tenantA, tenantB, userA, projectB };\n}\n\ndescribe('Tenant Isolation', () => {\n  let data;\n\n  beforeAll(async () => {\n    data = await seedTestData();\n  });\n\n  afterAll(async () => {\n    await pool.query(`DELETE FROM tenants WHERE name IN ('Tenant A', 'Tenant B')`);\n    await pool.end();\n    await redisClient.quit();  \/\/ close Redis connection so Jest exits cleanly\n  });\n\n  test('Tenant A user cannot read Tenant B project', async () => {\n    const token = generateToken({\n      userId:   data.userA,\n      tenantId: data.tenantA,   \/\/ \u2190 Tenant A token\n      email:    'usera@a.com',\n      role:     'Member',\n    });\n\n    const res = await request(app)\n      .get(`\/api\/projects\/${data.projectB}`)  \/\/ \u2190 Tenant B's project ID\n      .set('Authorization', `Bearer ${token}`);\n\n    \/\/ Must be 404, not 200 or 403\n    expect(res.status).toBe(404);\n  });\n\n  test('Tenant A user cannot list Tenant B projects', async () => {\n    const token = generateToken({\n      userId:   data.userA,\n      tenantId: data.tenantA,\n      email:    'usera@a.com',\n      role:     'TenantAdmin',\n    });\n\n    const res = await request(app)\n      .get('\/api\/projects')\n      .set('Authorization', `Bearer ${token}`);\n\n    expect(res.status).toBe(200);\n    \/\/ Response must contain zero Tenant B projects\n    const names = res.body.map(p => p.name);\n    expect(names).not.toContain('TEST-Secret Project');\n  });\n\n  test('Viewer cannot delete a project', async () => {\n    const token = generateToken({\n      userId:   data.userA,\n      tenantId: data.tenantA,\n      email:    'usera@a.com',\n      role:     'Viewer',         \/\/ \u2190 Viewer role\n    });\n\n    const res = await request(app)\n      .delete(`\/api\/projects\/${data.projectB}`)\n      .set('Authorization', `Bearer ${token}`);\n\n    expect(res.status).toBe(403);\n  });\n});\n<\/code><\/pre>\n<p>\u0679\u06cc\u0633\u0679 \u0686\u0644\u0627\u0626\u06cc\u06ba\u06d4<\/p>\n<pre><code class=\"language-bash\">npm test\n<\/code><\/pre>\n<p>\u062a\u06cc\u0646 \u0679\u06cc\u0633\u0679\u060c \u062a\u06cc\u0646 \u062d\u062f\u0648\u062f \u06a9\u06cc \u0646\u0634\u0627\u0646\u062f\u06c1\u06cc \u06a9\u06cc \u06af\u0626\u06cc\u06d4 \u06c1\u0631 \u067e\u0644 \u06a9\u06cc \u062f\u0631\u062e\u0648\u0627\u0633\u062a \u067e\u0631 \u0686\u0644\u0627\u0646\u06d2 \u06a9\u06d2 \u0644\u06cc\u06d2 \u0627\u0633\u06d2 \u0627\u067e\u0646\u06d2 CI \u0633\u06d2 \u062c\u0648\u0691\u06cc\u06ba\u06d4 \u0645\u0633\u062a\u0642\u0628\u0644 \u06a9\u06cc \u0631\u06cc \u0641\u06cc\u06a9\u0679\u0631\u0646\u06af \u062c\u0648 \u062e\u0627\u0645\u0648\u0634\u06cc \u0633\u06d2 \u062d\u0630\u0641 \u06c1\u0648 \u062c\u0627\u062a\u06cc \u06c1\u06d2\u06d4 <code>tenant_id<\/code> \u0641\u0644\u0679\u0631\u0632 \u0634\u067e\u0646\u06af \u0633\u06d2 \u067e\u06c1\u0644\u06d2 \u067e\u06a9\u0691\u06d2 \u062c\u0627\u0626\u06cc\u06ba \u06af\u06d2\u06d4<\/p>\n<h2 id=\"heading-troubleshooting\">\u0645\u0633\u0626\u0644\u06c1 \u062d\u0644<\/h2>\n<h3 id=\"heading-tenant-a-can-see-tenant-bs-data\">\u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 A \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 B \u06a9\u0627 \u0688\u06cc\u0679\u0627 \u062f\u06cc\u06a9\u06be \u0633\u06a9\u062a\u0627 \u06c1\u06d2\u06d4<\/h3>\n<p>\u0627\u06cc\u06a9 \u0633\u0648\u0627\u0644 \u0645\u06cc\u06ba <code>AND tenant_id = $N<\/code> I. \u062a\u0645\u0627\u0645 \u0631\u06cc\u067e\u0648\u0632\u0679\u0631\u06cc \u0641\u0627\u0626\u0644\u0648\u06ba \u06a9\u0648 \u062a\u0644\u0627\u0634 \u06a9\u0631\u06cc\u06ba\u06d4 <code>SELECT<\/code> \u0628\u06cc\u0627\u0646\u0627\u062a \u06a9\u0648 \u0686\u06cc\u06a9 \u06a9\u0631\u06cc\u06ba \u0627\u0648\u0631 \u06c1\u0631 \u0627\u06cc\u06a9 \u06a9\u0648 \u0686\u06cc\u06a9 \u06a9\u0631\u06cc\u06ba\u06d4 \u06cc\u06c1 \u062a\u0642\u0631\u06cc\u0628\u0627 \u06c1\u0645\u06cc\u0634\u06c1 \u0627\u0633 \u0637\u0631\u062d \u06c1\u06d2.<\/p>\n<h3 id=\"heading-403-forbidden-on-a-route-that-should-be-accessible\"><code>403 Forbidden<\/code>    \u0627\u0646 \u0631\u0627\u0633\u062a\u0648\u06ba \u067e\u0631 \u062c\u0648 \u0642\u0627\u0628\u0644 \u0631\u0633\u0627\u0626\u06cc \u06c1\u0648\u0646\u0627 \u0636\u0631\u0648\u0631\u06cc \u06c1\u06d2\u06d4<\/h3>\n<p>JWT \u0645\u06cc\u06ba \u0631\u0648\u0644 \u0633\u0679\u0631\u0646\u06af \u0627\u0633 \u0633\u06d2 \u0645\u06cc\u0644 \u0646\u06c1\u06cc\u06ba \u06a9\u06be\u0627\u062a\u06cc \u062c\u0648 \u06cc\u06c1 \u06c1\u06d2\u06d4 <code>requireRole()<\/code> \u0686\u06cc\u06a9 \u06a9\u0631 \u0631\u06c1\u0627 \u06c1\u06d2\u06d4 \u0639\u06cc\u0646 \u0645\u0637\u0627\u0628\u0642 \u0633\u0679\u0631\u0646\u06af \u06a9\u06d2 \u0644\u06cc\u06d2 \u0679\u0648\u06a9\u0646 \u067e\u06d2 \u0644\u0648\u0688 \u06a9\u0648 \u0686\u06cc\u06a9 \u06a9\u0631\u06cc\u06ba\u06d4 <code>'member'<\/code> \u0627\u0648\u0631 <code>'Member'<\/code> \u06cc\u06c1 \u0627\u06cc\u06a9 \u06c1\u06cc \u0686\u06cc\u0632 \u0646\u06c1\u06cc\u06ba \u06c1\u06d2\u06d4 \u0679\u0648\u06a9\u0646 \u06a9\u0648 jwt.io \u0645\u06cc\u06ba \u0686\u0633\u067e\u0627\u06ba \u06a9\u0631\u06cc\u06ba \u0627\u0648\u0631 \u0631\u0648\u0644 \u0641\u06cc\u0644\u0688 \u06a9\u0648 \u0628\u0631\u0627\u06c1 \u0631\u0627\u0633\u062a \u062f\u06cc\u06a9\u06be\u06cc\u06ba\u06d4<\/p>\n<h3 id=\"heading-rate-limiter-isnt-working\">\u0633\u067e\u06cc\u0688 \u0644\u06cc\u0645\u06cc\u0679\u0631 \u06a9\u0627\u0645 \u0646\u06c1\u06cc\u06ba \u06a9\u0631 \u0631\u06c1\u0627 \u06c1\u06d2\u06d4<\/h3>\n<p>\u0627\u06cc\u0633\u0627 \u0644\u06af\u062a\u0627 \u06c1\u06d2 \u06a9\u06c1 Redis \u0645\u0646\u0633\u0644\u06a9 \u0646\u06c1\u06cc\u06ba \u06c1\u06d2\u06d4 \u0644\u0627\u06af <code>redisClient.status<\/code> \u0633\u0631\u0648\u0631 \u0634\u0631\u0648\u0639 \u06c1\u0648\u0646\u06d2 \u0633\u06d2 \u067e\u06c1\u0644\u06d2\u06d4 \u0627\u06af\u0631 \u0646\u06c1\u06cc\u06ba <code>ready<\/code>\u0645\u062d\u062f\u0648\u062f \u06a9\u0631\u0646\u06d2 \u0648\u0627\u0644\u0627 \u06cc\u0627\u062f\u062f\u0627\u0634\u062a \u0645\u06cc\u06ba \u0648\u0627\u067e\u0633 \u0622 \u06af\u06cc\u0627 \u06c1\u06d2\u06d4 \u0627\u0633 \u06a9\u0627 \u0645\u0637\u0644\u0628 \u06cc\u06c1 \u06c1\u06d2 \u06a9\u06c1 \u062f\u0648\u0628\u0627\u0631\u06c1 \u0634\u0631\u0648\u0639 \u06a9\u0631\u0646\u06d2 \u0633\u06d2 \u062a\u0645\u0627\u0645 \u06a9\u0627\u0624\u0646\u0679\u0631\u0632 \u06a9\u0648 \u062f\u0648\u0628\u0627\u0631\u06c1 \u062a\u0631\u062a\u06cc\u0628 \u062f\u06cc\u0627 \u062c\u0627\u0626\u06d2 \u06af\u0627 \u0627\u0648\u0631 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06d2 \u062f\u0627\u0626\u0631\u06c1 \u06a9\u0627\u0631 \u06a9\u06cc \u062d\u062f\u0648\u062f \u06a9\u0627\u0645 \u06a9\u0631\u0646\u0627 \u0628\u0646\u062f \u06a9\u0631 \u062f\u06cc\u06ba \u06af\u06cc\u06d4<\/p>\n<h3 id=\"heading-audit-log-table-growing-very-large\">\u0622\u0688\u0679 \u0644\u0627\u06af \u0679\u06cc\u0628\u0644 \u0628\u06c1\u062a \u0628\u0691\u0627 \u06c1\u0648 \u0631\u06c1\u0627 \u06c1\u06d2\u06d4<\/h3>\n<p>\u06cc\u06c1 \u0645\u062a\u0648\u0642\u0639 \u0631\u0648\u06cc\u06c1 \u06c1\u06d2\u06d4 \u06a9\u0644\u06cc\u062f \u0622\u0688\u0679 \u0679\u06cc\u0628\u0644 \u06a9\u0648 \u0628\u0691\u06be\u0627\u0646\u0627 \u06c1\u06d2\u06d4 \u062c\u06cc\u0633\u06d2 \u062c\u06cc\u0633\u06d2 \u0633\u0627\u0626\u0632 \u0628\u0691\u06be\u062a\u0627 \u06c1\u06d2\u060c \u0627\u06cc\u06a9 \u0633\u0627\u0644 \u0633\u06d2 \u0632\u06cc\u0627\u062f\u06c1 \u067e\u0631\u0627\u0646\u06cc \u0642\u0637\u0627\u0631\u06cc\u06ba S3 \u06cc\u0627 Azure Blob \u067e\u0631 \u067e\u06c1\u0646\u0686\u0627\u0626\u06cc\u06ba \u0627\u0648\u0631 \u0686\u06be\u0648\u0679\u06cc \u06af\u0631\u0645 \u0645\u06cc\u0632\u0648\u06ba \u0633\u06d2 \u0627\u0633\u062a\u0641\u0633\u0627\u0631 \u062c\u0627\u0631\u06cc \u0631\u06a9\u06be\u06cc\u06ba\u06d4 \u0632\u06cc\u0627\u062f\u06c1 \u062a\u0631 \u062a\u0639\u0645\u06cc\u0644 \u06a9\u06d2 \u062a\u0642\u0627\u0636\u0648\u06ba \u06a9\u06d2 \u0644\u06cc\u06d2 \u0644\u0627\u06af\u0632 \u06a9\u0648 \u06a9\u0645 \u0627\u0632 \u06a9\u0645 12 \u0645\u0627\u06c1 \u062a\u06a9 \u0642\u0627\u0628\u0644 \u0631\u0633\u0627\u0626\u06cc \u06c1\u0648\u0646\u0627 \u0636\u0631\u0648\u0631\u06cc \u06c1\u06d2\u06d4 \u0645\u06cc\u0632 \u067e\u0631 \u06c1\u06cc DELETE \u0627\u0646\u062c\u0627\u0645 \u0646\u06c1 \u062f\u06cc\u06ba\u06d4<\/p>\n<h3 id=\"heading-jwtverify-throws-jsonwebtokenerror-invalid-signature\"><code>jwt.verify<\/code>    \u067e\u06be\u06cc\u0646\u06a9\u0646\u0627 <code>JsonWebTokenError: invalid signature<\/code><\/h3>\n<p>\u062c\u0633 \u067e\u0627\u0633 \u0648\u0631\u0688 \u0646\u06d2 \u0679\u0648\u06a9\u0646 \u067e\u0631 \u062f\u0633\u062a\u062e\u0637 \u06a9\u06cc\u06d2 \u0648\u06c1 \u0645\u0645\u0627\u062b\u0644 \u0646\u06c1\u06cc\u06ba \u06c1\u06d2\u06d4 <code>JWT_SECRET<\/code> \u0645\u0627\u062d\u0648\u0644 \u0645\u06cc\u06ba \u0622\u067e \u0627\u0633\u06d2 \u0686\u06cc\u06a9 \u06a9\u0631\u06cc\u06ba\u06d4 \u06cc\u06c1 \u0627\u06a9\u062b\u0631 \u0627\u0633 \u0648\u0642\u062a \u06c1\u0648\u062a\u0627 \u06c1\u06d2 \u062c\u0628 \u0645\u0627\u062d\u0648\u0644 \u06a9\u0648 \u062a\u0628\u062f\u06cc\u0644 \u06a9\u06cc\u0627 \u062c\u0627\u062a\u0627 \u06c1\u06d2 \u06cc\u0627 \u062c\u0628 \u062f\u0648\u0633\u0631\u06cc \u0633\u0631\u0648\u0633 \u06a9\u06cc \u0642\u062f\u0631\u06cc\u06ba \u0645\u062e\u062a\u0644\u0641 \u06c1\u0648\u062a\u06cc \u06c1\u06cc\u06ba\u06d4 <code>.env<\/code>. \u06a9\u0648\u0626\u06cc \u0628\u06be\u06cc \u0633\u0631\u0648\u0633 \u062c\u0633\u06d2 \u0622\u067e \u06a9\u0627\u0644 \u06a9\u0631\u06cc\u06ba\u06d4 <code>jwt.verify<\/code> \u06c1\u0645\u06cc\u06ba \u0627\u0633\u06cc \u0631\u0627\u0632 \u06a9\u06cc \u0636\u0631\u0648\u0631\u062a \u06c1\u06d2\u06d4 \u06a9\u0627\u067e\u06cc \u0627\u0648\u0631 \u062f\u0648\u0628\u0627\u0631\u06c1 \u0679\u0627\u0626\u067e \u0646\u06c1 \u06a9\u0631\u06cc\u06ba\u06d4<\/p>\n<h2 id=\"heading-wrapping-up\">\u062e\u062a\u0645<\/h2>\n<p>\u06c1\u0645 \u0646\u06d2 \u062c\u0648 \u0646\u0638\u0627\u0645 \u0628\u0646\u0627\u06cc\u0627 \u06c1\u06d2: \u0633\u0679\u0648\u0631\u06cc\u062c \u06a9\u06cc \u0642\u0637\u0627\u0631 \u06a9\u06cc \u0633\u0637\u062d \u067e\u0631 \u062a\u0646\u06c1\u0627\u0626\u06cc\u060c \u06c1\u06cc\u0646\u0688\u0644\u0631\u0632 \u06a9\u06d2 \u0686\u0644\u0646\u06d2 \u0633\u06d2 \u067e\u06c1\u0644\u06d2 \u06a9\u0631\u062f\u0627\u0631 \u06a9\u06cc \u062c\u0627\u0646\u0686\u060c \u0622\u0688\u0679 \u0679\u06cc\u0628\u0644\u0632 \u062c\u0646\u06c1\u06cc\u06ba \u0627\u06cc\u067e\u0633 \u0686\u06be\u0648 \u0646\u06c1\u06cc\u06ba \u0633\u06a9\u062a\u06cc\u060c \u0627\u0648\u0631 \u0641\u06cc \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06cc \u0634\u0631\u062d \u06a9\u0648 \u0645\u062d\u062f\u0648\u062f \u06a9\u0631\u0646\u0627\u06d4 \u0628\u0633\u06d4<\/p>\n<p>\u0679\u06cc\u0633\u0679 \u0648\u06c1 \u06c1\u06d2 \u062c\u0633\u06d2 \u0645\u06cc\u06ba \u0627\u06a9\u062b\u0631 \u062a\u0648\u0691\u062a\u0627 \u06c1\u0648\u06ba\u06d4 \u0679\u06cc\u0645 \u0622\u0626\u0633\u0648\u0644\u06cc\u0634\u0646 \u0628\u0646\u0627\u062a\u06cc \u06c1\u06d2 \u0627\u0648\u0631 \u0628\u06be\u06cc\u062c\u062a\u06cc \u06c1\u06d2 \u0627\u0648\u0631 \u062d\u0642\u06cc\u0642\u062a \u0645\u06cc\u06ba \u06cc\u06c1 \u062b\u0627\u0628\u062a \u06a9\u0631\u0646\u06d2 \u06a9\u06d2 \u0644\u06cc\u06d2 \u06a9\u0686\u06be \u0646\u06c1\u06cc\u06ba \u0644\u06a9\u06be\u062a\u06cc \u06c1\u06d2 \u06a9\u06c1 \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631\u0648\u06ba \u06a9\u06d2 \u062f\u0631\u0645\u06cc\u0627\u0646 \u0688\u06cc\u0679\u0627 \u0644\u06cc\u06a9 \u0646\u06c1\u06cc\u06ba \u06c1\u0648 \u0633\u06a9\u062a\u0627\u06d4 \u0627\u0633 \u06a9\u06d2 \u0628\u0639\u062f \u0627\u0633\u062a\u0641\u0633\u0627\u0631 6 \u0645\u0627\u06c1 \u0628\u0639\u062f \u062f\u0648\u0628\u0627\u0631\u06c1 \u06a9\u06cc\u0627 \u062c\u0627\u062a\u0627 \u06c1\u06d2 \u0627\u0648\u0631 <code>tenant_id<\/code> \u0641\u0644\u0679\u0631 \u062e\u0627\u0645\u0648\u0634\u06cc \u0633\u06d2 \u063a\u0627\u0626\u0628 \u06c1\u0648 \u062c\u0627\u062a\u0627 \u06c1\u06d2\u06d4 CI \u0646\u06d2 \u0627\u0633\u06d2 \u067e\u06a9\u0691 \u0644\u06cc\u0627\u06d4 \u062f\u0633\u062a\u06cc \u06a9\u0648\u0688 \u06a9\u0627 \u062c\u0627\u0626\u0632\u06c1 \u0634\u0627\u0630 \u0648 \u0646\u0627\u062f\u0631 \u06c1\u06cc \u0627\u0646\u062c\u0627\u0645 \u062f\u06cc\u0627 \u062c\u0627\u062a\u0627 \u06c1\u06d2\u06d4<\/p>\n<p>\u0627\u06cc\u06a9 \u0628\u0627\u0631 \u062c\u0628 \u0622\u067e \u06a9\u0627 \u067e\u0631\u0648\u0688\u06a9\u0679 \u06a9\u0627\u0641\u06cc \u0628\u0691\u0627 \u06c1\u0648 \u062c\u0627\u062a\u0627 \u06c1\u06d2\u060c \u06a9\u0631\u0627\u06cc\u06c1 \u062f\u0627\u0631 \u06a9\u06d2 \u0644\u06cc\u06d2 \u0645\u062e\u0635\u0648\u0635 \u0627\u0633\u06a9\u06cc\u0645\u06d2 \u0622\u062e\u0631 \u06a9\u0627\u0631 \u0633\u0627\u0645\u0646\u06d2 \u0622\u0626\u06cc\u06ba \u06af\u06d2\u06d4 \u0644\u06cc\u06a9\u0646 \u067e\u06c1\u0644\u06d2 \u0627\u06cc\u0633\u0627 \u0646\u06c1\u06cc\u06ba \u062a\u06be\u0627\u06d4 \u0642\u0637\u0627\u0631 \u06a9\u06cc \u0633\u0637\u062d \u06a9\u06cc \u062a\u0646\u06c1\u0627\u0626\u06cc \u0632\u06cc\u0627\u062f\u06c1 \u062a\u0631 \u0679\u06cc\u0645\u0648\u06ba \u0633\u06d2 \u0632\u06cc\u0627\u062f\u06c1 \u067e\u06cc\u0645\u0627\u0646\u06d2 \u067e\u0631 \u06c1\u06cc\u0646\u0688\u0644 \u06a9\u0631\u062a\u06cc \u06c1\u06d2 \u0627\u0648\u0631 \u0622\u067e\u0631\u06cc\u0634\u0646\u0644 \u0627\u0648\u0648\u0631 \u06c1\u06cc\u0688 \u06a9\u0627 \u0635\u0631\u0641 \u0627\u06cc\u06a9 \u062d\u0635\u06c1 \u0644\u06cc\u062a\u06cc \u06c1\u06d2\u06d4<\/p>\n<p>\u0645\u06a9\u0645\u0644 \u0648\u0631\u06a9\u0646\u06af \u06a9\u0648\u0688 GitHub \u067e\u0631 \u067e\u0627\u06cc\u0627 \u062c\u0627 \u0633\u06a9\u062a\u0627 \u06c1\u06d2: nodejs-multitenant-saas-api.<\/p>\n<\/p><\/div>\n","protected":false},"excerpt":{"rendered":"<p>\u0627\u06cc\u06a9 \u0633\u0627\u062a\u06be\u06cc \u0646\u06d2 \u0645\u062c\u06be \u0633\u06d2 \u06a9\u06c1\u0627 \u06a9\u06c1 \u0627\u0633 \u06a9\u0648 \u0688\u06cc\u0628\u06af \u06a9\u0631\u0646\u06d2 \u0645\u06cc\u06ba \u0645\u062f\u062f \u06a9\u0631\u0648\u06ba \u062c\u0648 \u0627\u0633 \u06a9\u06d2 SaaS \u067e\u0631\u0648\u062c\u06cc\u06a9\u0679 \u0645\u06cc\u0646\u062c\u0645\u0646\u0679 \u0679\u0648\u0644 \u0645\u06cc\u06ba \u0627\u062c\u0627\u0632\u062a \u06a9\u06d2 \u0645\u0633\u0626\u0644\u06d2 \u06a9\u06cc \u0637\u0631\u062d \u0644\u06af\u062a\u0627 \u06c1\u06d2\u06d4 \u0635\u0627\u0631\u0641\u06cc\u0646 \u0648\u06c1 \u0648\u0633\u0627\u0626\u0644 \u062f\u06cc\u06a9\u06be \u0631\u06c1\u06d2 \u062a\u06be\u06d2 \u062c\u0648 \u0627\u0646\u06c1\u0648\u06ba \u0646\u06d2 \u0646\u06c1\u06cc\u06ba \u0628\u0646\u0627\u0626\u06d2 \u062a\u06be\u06d2\u06d4 \u0645\u06cc\u06ba \u0646\u06d2 \u0627\u0633\u062a\u0641\u0633\u0627\u0631 \u0644\u0627\u06af \u0628\u0627\u06c1\u0631 \u0646\u06a9\u0627\u0644\u0627\u060c \u06a9\u0686\u06be \u0679\u06be\u06cc\u06a9 \u0679\u06be\u06cc\u06a9 \u06a9\u06cc \u0627\u0645\u06cc\u062f \u0645\u06cc\u06ba\u06d4 \u0627\u06cc\u0633\u0627 \u0646\u06c1\u06cc\u06ba [&hellip;]<\/p>\n","protected":false},"author":7,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[1],"tags":[],"class_list":["post-27098","post","type-post","status-publish","format-standard","hentry","category-blog"],"_links":{"self":[{"href":"https:\/\/umang.pk\/ur\/wp-json\/wp\/v2\/posts\/27098","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/umang.pk\/ur\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/umang.pk\/ur\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/umang.pk\/ur\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/umang.pk\/ur\/wp-json\/wp\/v2\/comments?post=27098"}],"version-history":[{"count":0,"href":"https:\/\/umang.pk\/ur\/wp-json\/wp\/v2\/posts\/27098\/revisions"}],"wp:attachment":[{"href":"https:\/\/umang.pk\/ur\/wp-json\/wp\/v2\/media?parent=27098"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/umang.pk\/ur\/wp-json\/wp\/v2\/categories?post=27098"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/umang.pk\/ur\/wp-json\/wp\/v2\/tags?post=27098"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}