B2B संगठनों और टीमों का डेटाबेस स्कीमा जो लंबे समय तक समझ में रहे
B2B संगठनों और टीमों के लिए डेटाबेस स्कीमा: निमंत्रण, सदस्यता स्थितियाँ, रोल इनहेरिटेंस और ऑडिट-तैयार परिवर्तन के लिए एक व्यावहारिक रिलेशनल पैटर्न।

यह पैटर्न किस समस्या को हल करता है
अधिकांश B2B ऐप्स सचमुच "यूजर अकाउंट्स" ऐप नहीं होते। वे साझा वर्कस्पेस होते हैं जहाँ लोग किसी संगठन के हिस्से होते हैं, टीमों में बंटे होते हैं, और उनकी अनुमति उनके काम के अनुसार बदलती रहती है। सेल्स, सपोर्ट, फ़ाइनेंस और एडमिन्स को अलग एक्सेस चाहिए, और वह एक्सेस समय के साथ बदलता है।
एक बहुत ही साधारण मॉडल जल्दी टूट जाता है। अगर आप सिर्फ एक users तालिका में एक ही role कॉलम रखते हैं, तो आप यह व्यक्त नहीं कर पाएँगे कि "वही व्यक्ति एक org में Admin है, लेकिन दूसरे में Viewer।" आप आम मामलों को भी हैंडल नहीं कर पाएँगे, जैसे कि कॉन्ट्रैक्टर्स जिन्हें केवल एक टीम दिखाई देनी चाहिए, या कोई कर्मचारी जो किसी प्रोजेक्ट छोड़ दे पर कंपनी में अभी भी रहता हो।
इनवाइट्स भी बग का एक सामान्य स्रोत होते हैं। अगर एक invitation सिर्फ एक ईमेल रो है, तो यह अस्पष्ट हो जाता है कि व्यक्ति अभी org में "है" या नहीं, उन्हें किस टीम में आना है, और अगर वे किसी अलग ईमेल से साइन अप करते हैं तो क्या होगा। यहाँ छोटी असंगतियाँ अक्सर सुरक्षा समस्याओं में बदल जाती हैं।
यह पैटर्न चार लक्ष्यों की दिशा में काम करता है:
- सुरक्षा: अनुमतियाँ स्पष्ट सदस्यता से आती हैं, धारणाओं से नहीं।
- स्पष्टता: orgs, teams, और roles के लिए हर एक की एक सच्चाई का स्रोत हो।
- संगति: निमंत्रण और सदस्यताएँ एक पूर्वानुमेय लाइफसाइकल फ़ॉलो करें।
- इतिहास: आप बता सकें कि किसने एक्सेस दिया, रोल बदला, या किसी को निकाला।
वादा यह है कि एक ही रिलेशनल मॉडल जो फीचर बढ़ने पर भी समझने में सरल रहे: एक यूजर के कई orgs हो सकते हैं, एक org में कई टीमें हो सकती हैं, रोल इनहेरिटेंस पूर्वानुमेय हो, और परिवर्तन ऑडिट-फ्रेंडली हों। यह ऐसी संरचना है जिसे आप आज लागू कर सकते हैं और बाद में बिना सब कुछ फिर से लिखे बढ़ा सकते हैं।
मुख्य शर्तें: orgs, teams, users, और memberships
अगर आप ऐसा स्कीमा चाहते हैं जो छह महीने बाद भी पढ़ने में आसान रहे, तो कुछ शब्दों पर पहले सहमति बनाइए। अधिकांश भ्रम "कौन कौन है" और "वे क्या कर सकते हैं" को मिलाकर आता है।
एक Organization (org) शीर्ष टेनेंट सीमा है। यह ग्राहक या बिजनेस अकाउंट को दर्शाता जो डेटा का मालिक है। अगर दो यूजर अलग orgs में हैं, तो डिफ़ॉल्ट रूप से उन्हें एक-दूसरे का डेटा नहीं दिखना चाहिए। यह नियम कई आकस्मिक क्रॉस-टेनेंट एक्सेस को रोकता है।
एक Team org के अंदर एक छोटी इकाई है। टीमें असली कार्य इकाइयों को मॉडल करती हैं: Sales, Support, Finance, या "Project A"। टीमें org सीमा की जगह नहीं लेतीं; वे उसी के अंतर्गत रहती हैं।
एक User एक पहचान है। यह व्यक्ति का लॉगिन और प्रोफ़ाइल है: ईमेल, नाम, पासवर्ड या SSO ID, और शायद MFA सेटिंग्स। एक यूजर बिना किसी एक्सेस के भी मौजूद रह सकता है।
एक Membership वह एक्सेस रिकॉर्ड है। यह बताता है: “यह यूजर इस org (और वैकल्पिक रूप से इस टीम) का सदस्य है, इस स्टेटस और इन रोल्स के साथ।” पहचान (User) को एक्सेस (Membership) से अलग रखना कॉन्ट्रैक्टर्स, ऑफबोर्डिंग, और मल्टी-ऑर्ग एक्सेस को मॉडल करने में बहुत आसान बनाता है।
कोड और UI में उपयोग के लिए सरल अर्थ:
- Member: एक यूजर जिसके पास org या टीम में सक्रिय सदस्यता है।
- Role: अनुमति का नामित समूह (उदाहरण: Org Admin, Team Manager)।
- Permission: एक इजाज़त दी गई कार्रवाई (उदाहरण: “view invoices”)।
- Tenant boundary: नियम कि डेटा किसी org तक सीमित है।
सदस्यता को एक छोटा स्टेट मशीन मानें, न कि boolean। सामान्य अवस्थाएँ हैं invited, active, suspended, और removed। इससे निमंत्रण, अनुमोदन, और ऑफबोर्डिंग सुसंगत और ऑडिटेबल रहते हैं।
एकल रिलेशनल मॉडल: मुख्य तालिकाएँ और रिश्ते
एक अच्छा मल्टी-टेनेन्ट स्कीमा एक विचार से शुरू होता है: "किसका कहाँ हिस्सा है" यह एक ही जगह पर स्टोर करें, और बाकी सब समर्थन तालिकाएँ रहें। इस तरह आप बुनियादी प्रश्नों का उत्तर दे सकते हैं (कौन org में है, किस टीम में है, वे क्या कर सकते हैं) बिना असंबंधित मॉडलों के बीच सछिद्र (hopping) किए।
आम तौर पर जिन मुख्य तालिकाओं की ज़रूरत होती है:
- organizations: हर ग्राहक अकाउंट के लिए एक रो। नाम, स्टेटस, बिलिंग फ़ील्ड, और एक अपरिवर्तनीय id रखती है।
- teams: संगठन के अंदर समूह (Support, Sales, Admin)। हमेशा एक organization से संबंधित रहती है।
- users: हर व्यक्ति के लिए एक रो। यह ग्लोबल है, प्रति-ऑर्ग नहीं।
- memberships: वह ब्रिज जो बताता है “यह यूजर इस संगठन का सदस्य है” और वैकल्पिक रूप से "इस टीम का भी"।
- role_grants (या role_assignments): किस membership को कौन से रोल मिले हैं, org स्तर पर, टीम स्तर पर, या दोनों पर।
कुंजी और कंस्ट्रेंट्स कड़े रखें। प्रत्येक तालिका के लिए सर्वनामक प्राथमिक कुंजियाँ (UUIDs या bigints) उपयोग करें। ऐसे विदेशी कुंजियाँ जोड़ें जैसे teams.organization_id -> organizations.id और memberships.user_id -> users.id। फिर कुछ यूनिक कंस्ट्रेंट्स जोड़ें ताकि डुप्लिकेट्स प्रोडक्शन में आने से पहले रोके जा सकें।
गलत डेटा को जल्दी पकड़ने वाले नियम:
- एक org slug या बाहरी कुंजी:
unique(organizations.slug) - org के भीतर टीम नाम यूनिक:
unique(teams.organization_id, teams.name) - कोई डुप्लिकेट org सदस्यता न हो:
unique(memberships.organization_id, memberships.user_id) - अगर आप टीम सदस्यता अलग रखते हैं तो डुप्लिकेट टीम सदस्यता न हो:
unique(team_memberships.team_id, team_memberships.user_id)
निर्धारित करें क्या append-only होगा और क्या updateable। Organizations, teams, और users updateable हैं। Memberships अक्सर वर्तमान स्थिति (active, suspended) के लिए अपडेटेबल होते हैं, पर परिवर्तन के साथ-साथ एक append-only access log लिखना चाहिए ताकि बाद में ऑडिट आसान हो।
निमंत्रण और सदस्यता स्थितियाँ जो सुसंगत रहें
एक्सेस को साफ़ रखने का सबसे आसान तरीका है कि निमंत्रण को अपना अलग रिकॉर्ड मानें, न कि आधा बन चुकी सदस्यता। एक membership का मतलब है "इस यूजर के पास वर्तमान में एक्सेस है।" एक invitation का मतलब है "हमने एक्सेस ऑफर किया, पर यह अभी वास्तविक नहीं है।" उन्हें अलग रखने से घोस्ट मेंबर, आधी बनी अनुमतियाँ, और "किसने इस व्यक्ति को बुलाया?" जैसी गुत्थियाँ टलती हैं।
एक सरल, भरोसेमंद स्टेट मॉडल
Memberships के लिए एक छोटा सेट रखें जिसे आप किसी को भी समझा सकें:
active: यूजर org (और जिन भी टीमों का सदस्य है) में एक्सेस कर सकता हैsuspended: अस्थायी रूप से ब्लॉक किया गया, पर इतिहास बना रहता हैremoved: अब सदस्य नहीं है, ऑडिट और रिपोर्टिंग के लिए रखा गया
कई टीमें membership में invited स्टेट लेने से बचती हैं और invited को सख़्ती से invitations टेबल में रखती हैं। यह साफ़ रहता है: membership रो केवल उन यूज़र्स के लिए होती है जिनके पास असल में एक्सेस है (active), या जिनके पास पहले था (suspended/removed)।
जब अकाउंट अभी नहीं है तब ईमेल इनवाइट्स
B2B ऐप्स अक्सर किसी यूजर अकाउंट के बिना ईमेल पर इनवाइट भेजते हैं। invitation रिकॉर्ड पर ईमेल स्टोर करें, साथ में जहाँ इनवाइट लागू होती है (org या team), इच्छित रोल, और किसने भेजा। अगर व्यक्ति बाद में उसी ईमेल से साइन अप करता है, तो आप पेंडिंग इनवाइट्स से मैच कर सकते हैं और उन्हें स्वीकार करने दे सकते हैं।
जब इनवाइट स्वीकार हो, उसे एक लेनदेन में हैंडल करें: invitation को accepted मार्क करें, membership बनाएं, और एक ऑडिट एंट्री लिखें (किसने स्वीकार किया, कब, और किस ईमेल से)।
स्पष्ट इनवाइट एंड स्टेट्स परिभाषित करें:
expired: इसकी समय-सीमा गुजर चुकी है और स्वीकार नहीं की जा सकतीrevoked: किसी एडमिन द्वारा रद्द की गई और स्वीकार नहीं की जा सकतीaccepted: membership में बदली गई
"एक पेंडिंग इनवाइट प्रति org या टीम प्रति ईमेल" लागू करके डुप्लिकेट इनवाइट्स रोकें। अगर आप री-इनवाइट सपोर्ट करते हैं, तो या तो मौजूदा पेंडिंग इनवाइट की एक्सपायरी बढ़ाएँ या पुरानी को revoke करके नया टोकन जारी करें।
रोल और इनहेरिटेंस बिना एक्सेस को गोंदा किए
अधिकांश B2B ऐप्स को दो स्तर की एक्सेस की जरूरत होती है: संगठन स्तर पर क्या कर सकता है, और किसी खास टीम के अंदर क्या कर सकता है। इन्हें एक role फ़ील्ड में मिलाना ही असंगति की शुरुआत है।
Org-स्तर रोल्स यह तय करते हैं कि क्या कोई व्यक्ति बिलिंग मैनेज कर सकता है, लोगों को इनवाइट कर सकता है, या सभी टीमों को देख सकता है। टीम-स्तर रोल्स यह तय करते हैं कि क्या वे Team A में आइटम एडिट कर सकते हैं, Team B में रिक्वेस्ट अप्रूव कर सकते हैं, या सिर्फ़ देख सकते हैं।
रोल इनहेरिटेंस तब सबसे आसान रहती है जब यह एक नियम फ़ॉलो करे: एक org रोल हर जगह लागू होता है जब तक कि किसी टीम ने स्पष्ट रूप से अलग न बताया हो। इससे व्यवहार पूर्वानुमेय रहता है और डुप्लिकेट डेटा कम होता है।
इसे मॉडल करने का एक साफ़ तरीका है रोल असाइनमेंट्स को स्कोप के साथ स्टोर करना:
role_assignments:user_id,org_id, वैकल्पिकteam_id(NULL का अर्थ org-विस्तृत),role_id,created_at,created_by
अगर आप चाहते हैं कि "प्रति स्कोप एक रोल ही हो", तो (user_id, org_id, team_id) पर एक यूनिक कंस्ट्रेंट जोड़ें।
फिर किसी टीम के लिए इफेक्टिव एक्सेस बनने का क्रम है:
-
टीम-विशिष्ट असाइनमेंट खोजें (
team_id = X)। अगर मौजूद है, तो वही इस्तेमाल करें। -
अन्यथा org-व्यापी असाइनमेंट पर वापस आएँ (
team_id IS NULL)।
न्यूनतम-विशेषाधिकार (least-privilege) डिफ़ॉल्ट के लिए, एक न्यूनतम org रोल चुनें (अक्सर “Member”) और उसे छिपे हुए एडमिन अधिकार न दें। नए यूज़र्स को छिपकर टीम एक्सेस न दें। अगर आप ऑटो-ग्रांट करते हैं, तो इसे स्पष्ट टीम सदस्यताओं के रूप में करें, न कि org रोल की चुप्पी से विस्तृति करके।
ओवरराइड्स दुर्लभ और स्पष्ट होने चाहिए। उदाहरण: मारिया org में "Manager" है (invite कर सकती है, रिपोर्ट देख सकती है), पर Finance टीम में उसे "Viewer" होना चाहिए। आप मारिया की एक org-व्यापी असाइनमेंट स्टोर करें, और Finance के लिए एक टीम-स्कोप्ड ओवरराइड। कोई अनुमति कॉपी नहीं होती, और अपवाद दिखाई देता है।
रोल नाम आम पैटर्न के लिए काम आते हैं। एक्सप्लिसिट परमिशन्स तब उपयोग करें जब वास्तव में one-off ज़रूरत हो (जैसे "export कर सकता है पर edit नहीं कर सकता"), या जब अनुपालन के लिए स्पष्ट_ALLOWED_ACTIONS की सूची चाहिए। तब भी, स्कोप का वही विचार रखें ताकि मानसिक मॉडल सुसंगत रहे।
ऑडिट-फ्रेंडली परिवर्तन: किसने एक्सेस बदला ट्रैक करना
अगर आपका ऐप सिर्फ़ वर्तमान रोल को membership रो पर स्टोर करता है, तो आप कहानी खो देते हैं। जब कोई पूछे, "किसने Alex को पिछले मंगलवार admin एक्सेस दिया था?" तो आपके पास भरोसेमंद उत्तर नहीं होगा। आपको परिवर्तन का इतिहास चाहिए, न कि सिर्फ वर्तमान स्थिति।
सरल तरीका है एक समर्पित ऑडिट लॉग तालिका जो एक्सेस इवेंट्स रिकॉर्ड करे। इसे append-only जर्नल मानें: आप पुराने ऑडिट रो एडिट नहीं करते; बस नए जोड़ते हैं।
एक व्यावहारिक ऑडिट तालिका सामान्यतः शामिल करती है:
actor_user_id(जिसने परिवर्तन किया)subject_typeऔरsubject_id(membership, team, org)action(invite_sent, role_changed, membership_suspended, team_deleted)occurred_at(कब हुआ)reason(वैकल्पिक फ्री टेक्स्ट जैसे "contractor offboarding")
“before” और “after” कैप्चर करने के लिए, उन फील्ड्स का छोटा स्नैपशॉट रखें जो आपको अहम लगते हैं। इसे एक्सेस-कंट्रोल डेटा तक सीमित रखें, पूरा यूजर प्रोफ़ाइल नहीं। उदाहरण: before_role, after_role, before_state, after_state, before_team_id, after_team_id। अगर आप लचीलापन चाहते हैं तो दो JSON कॉलम (before, after) उपयोग कर सकते हैं, पर payload छोटा और सुसंगत रखें।
Memberships और teams के लिए soft delete आम तौर पर hard delete से बेहतर होता है। रो हटाने के बजाय उसे disabled मार्क करें, deleted_at और deleted_by जैसी फ़ील्ड्स रखें। इससे फॉरेन कीज़ अखंड रहती हैं और पिछले एक्सेस को समझना आसान होता है। निश्चित अस्थायी रेकॉर्ड्स (जैसे expired invites) के लिए hard delete समझ में आ सकता है, पर केवल जब आप सुनिश्चित हों कि आपको बाद में उनकी ज़रूरत नहीं पड़ेगी।
इसके साथ आप सामान्य अनुपालन प्रश्नों का तेज़ी से उत्तर दे सकते हैं:
- किसने या किसने एक्सेस दिया/हटाया, और कब?
- ठीक क्या बदला (role, team, state)?
- क्या एक्सेस सामान्य ऑफबोर्डिंग फ्लो का हिस्सा था?
चरण-दर-चरण: रिलेशनल डेटाबेस में स्कीमा डिज़ाइन करना
सरल से शुरू करें: किसका क्या हिस्सा है यह कहने के लिए एक जगह, और कारण। इसे छोटे चरणों में बनाएं, और नियम जोड़ें ताकि डेटा "लगभग सही" में न फँसे।
एक व्यावहारिक क्रम जो PostgreSQL और अन्य रिलेशनल DBs में अच्छी तरह काम करता है:
-
organizationsऔरteamsबनाइए, प्रत्येक के साथ स्थिर प्राथमिक कुंजी (UUID या bigint)।teams.organization_idको विदेशी कुंजी के रूप में जोड़ें, और जल्दी तय करें कि क्या टीम नाम org के अंदर यूनिक होने चाहिए। -
usersको सदस्यता से अलग रखें। पहचान फ़ील्डusersमें रखें (email, status, created_at)। "किसी org/टीम का सदस्य है" जैसे संबंधmembershipsटेबल में रखें जिसमेंuser_id,organization_id, वैकल्पिकteam_id, और एकstateकॉलम (active,suspended,removed) हो। -
invitationsको उसकी अलग तालिका बनाएं, membership पर नहीं।organization_id, वैकल्पिकteam_id,email,token,expires_at, औरaccepted_atस्टोर करें। "एक खुला इनवाइट प्रति org + email + team" की यूनीकनेस लागू करें ताकि डुप्लिकेट न बनें। -
रोल्स को एक्सप्लिसिट तालिकाओं से मॉडल करें। एक सरल तरीका है
roles(admin, member, आदि) औरrole_assignmentsजो या तो org स्कोप (कोईteam_idनहीं) या team स्कोप (team_idसेट) की ओर पॉइंट करें। इनहेरिटेंस नियम सुसंगत और टेस्टेबल रखें। -
पहले दिन से ऑडिट ट्रेल जोड़ें।
access_eventsटेबल का उपयोग करें जिसमेंactor_user_id,target_user_id(या invites के लिए ईमेल),action(invite_sent, role_changed, removed),scope(org/team), औरcreated_atहो।
इन तालिकाओं के बनने के बाद कुछ बेसिक एडमिन क्वेरीज़ चलाकर वास्तविकता को सत्यापित करें: "किसे org-विस्तृत एक्सेस है?", "किस टीम के पास कोई एडमिन नहीं है?", और "कौन से invites expired हैं पर अभी भी खुले हैं?" ये प्रश्न अक्सर शुरुआती कंस्ट्रेंट की कमी उजागर करते हैं।
गंदा डेटा रोकने वाले नियम और कंस्ट्रेंट्स
एक स्कीमा तब सुसंगत रहता है जब डेटाबेस, सिर्फ़ आपका कोड नहीं, टेनेंट सीमाओं को लागू करे। सरल नियम यह है: हर टेनेंट-स्कोप्ड टेबल org_id रखे, और हर लुकअप में यह शामिल हो। भले ही ऐप में कोई फ़िल्टर भूल जाए, डेटाबेस को क्रॉस-ऑर्ग कनेक्शनों का विरोध करना चाहिए।
गार्डरेल्स जो डेटा को साफ़ रखती हैं
ऐसे विदेशी कंस्ट्रेंट्स शुरु करें जो हमेशा "एक ही org के भीतर" पॉइंट करें। उदाहरण के लिए, अगर आप टीम सदस्यता अलग रखते हैं, तो team_memberships रो में team_id और user_id के साथ org_id भी होनी चाहिए। कंपोजिट कीज़ के साथ आप यह ज़रूरी बना सकते हैं कि संदर्भित टीम उसी org से تعلق रखती हो।
सबसे सामान्य समस्याओं को रोकने वाले कंस्ट्रेंट्स:
- प्रति यूजर प्रति org एक सक्रिय सदस्यता:
(org_id, user_id)पर यूनिक (जहाँ समर्थित हो partial condition के साथ active पंक्तियों के लिए)। - प्रति ईमेल प्रति org या टीम एक पेंडिंग इनवाइट:
(org_id, team_id, email)पर यूनिक जहाँstate = 'pending'। - इनवाइट टोकन ग्लोबली यूनिक और कभी री-यूज़ न हो:
unique(invite_token)। - टीम बिल्कुल एक org से संबंधित हो:
teams.org_idNOT NULL औरorgs(id)पर विदेशी कुंजी। - membership हटाने के बजाय समाप्त करें:
ended_at(और वैकल्पिकended_by) स्टोर करें ताकि ऑडिट इतिहास सुरक्षित रहे।
उन लुकअप्स के लिए इंडेक्स जो आप वाकई करते हैं
उन क्वेरीज़ पर इंडेक्स लगाएँ जो आपका ऐप बार-बार चलाता है:
(org_id, user_id)— "यह यूजर किन orgs में है?"(org_id, team_id)— "किस टीम के सदस्य कौन हैं?"(invite_token)— "इनवाइट स्वीकार करें"(org_id, state)— "पेंडिंग इनवाइट्स" और "सक्रिय मेंबर्स" के लिए
org नामों को बदलने में आसान रखें। हमेशा orgs.id का प्रयोग करें और orgs.name (और किसी भी slug) को संपादनयोग्य फील्ड मानें। नाम बदलने पर एक ही रो अपडेट होगी।
टीम को एक org से दूसरे में ले जाना आम तौर पर नीति निर्णय है। सबसे सुरक्षित विकल्प इसे मना कर देना (या टीम को क्लोन करना) है क्योंकि सदस्यताएँ, रोल्स, और ऑडिट इतिहास org-स्कोप्ड होते हैं। अगर आपको माइग्रेट करना ही है तो एक ही लेनदेन में करें और सभी चाइल्ड रोज़ के org_id अपडेट करें।
यूज़र्स के जाने पर orphan रेकॉर्ड रोकने के लिए hard deletes से बचें। यूजर को disabled करें, उनकी सदस्यताएँ खत्म करें, और parent rows पर deletes को रोकें (ON DELETE RESTRICT) जब तक कि आप वास्तव में cascading removal चाहते हों।
उदाहरण परिदृश्य: एक org, दो टीमें, सुरक्षित रूप से एक्सेस बदलना
कल्पना कीजिए Northwind Co नाम की एक कंपनी एक org और दो टीमों के साथ: Sales और Support। वे एक कॉन्ट्रैक्टर Mia को Support टिकटों पर एक महीने के लिए हायर करते हैं। मॉडल को यहाँ पूर्वानुमेय रहना चाहिए: एक व्यक्ति, एक org सदस्यता, वैकल्पिक टीम सदस्यताएँ, और स्पष्ट अवस्थाएँ।
एक org एडमिन (Ava) Mia को ईमेल से आमंत्रित करता है। सिस्टम org से जुड़ा एक invitation रो बनाता है, pending स्टेट और एक expiry तारीख के साथ। अभी कुछ भी बदला नहीं, इसलिए कोई "आधा यूजर" नहीं बनता जिसकी एक्सेस अस्पष्ट हो।
जब Mia स्वीकार करती है, invitation accepted मार्क होता है, और एक org membership रो active स्टेट के साथ बनती है। Ava Mia को org रोल member देती है (न कि admin)। फिर Ava Mia को Support टीम में जोड़ती है और टीम रोल जैसे support_agent असाइन करती है।
अब एक ट्विस्ट जोड़ें: Ben पूर्णकालिक कर्मचारी है और उसका org रोल admin है, पर उसे Support डेटा नहीं देखना चाहिए। आप इसे हैंडल कर सकते हैं एक टीम-लेवल ओवरराइड से जो स्पष्ट रूप से उसके Support टीम रोल को डाउनग्रेड कर दे जबकि उसका org-व्यापी एडमिन लाभ बरकरार रहे।
एक हफ्ते बाद, Mia ने पॉलिसी का उल्लंघन किया और उसे suspended किया गया। पंक्तियाँ हटाने के बजाय, Ava Mia की org membership का state suspended कर देती है। टीम सदस्यताएँ बनी रह सकती हैं पर वे अप्रभावी हो जाती हैं क्योंकि org membership सक्रिय नहीं है।
ऑडिट इतिहास साफ़ रहता है क्योंकि हर परिवर्तन एक इवेंट है:
- Ava ने Mia को आमंत्रित किया (कौन, क्या, कब)
- Mia ने इनवाइट स्वीकार किया
- Ava ने Mia को Support में जोड़ा और
support_agentअसाइन किया - Ava ने Ben के लिए Support ओवरराइड सेट किया
- Ava ने Mia को suspended किया
इस मॉडल से UI स्पष्ट एक्सेस सारांश दिखा सकता है: org स्टेटस (active या suspended), org रोल, टीम सूची रोल्स और ओवरराइड्स के साथ, और एक "हाल के एक्सेस परिवर्तन" फ़ीड जो बताता है कि किस वजह से किसी को Sales या Support नहीं दिखाई दे रहा।
सामान्य गलतियाँ और जाल जिनसे बचें
अधिकांश एक्सेस बग "लगभग सही" डेटा मॉडलों से आते हैं। स्कीमा शुरू में ठीक दिखता है, फिर एज केस इकट्ठा हो जाते हैं: री-इनवाइट्स, टीम मूव, रोल परिवर्तन, और ऑफबोर्डिंग।
एक सामान्य जाल है invitations और memberships को एक ही रो में मिलाना। अगर आप "invited" और "active" को एक ही रिकॉर्ड में बिना स्पष्ट अर्थ के रखते हैं, तो आप ऐसे प्रश्नों से जूझेंगे जैसे "क्या यह व्यक्ति सदस्य है अगर उसने कभी स्वीकार नहीं किया?" निमंत्रण और सदस्यता को अलग रखें, या स्टेट मशीन को स्पष्ट और सुसंगत बनाएं।
एक और आम गलती है users टेबल पर एक सिंगल role कॉलम रखना और काम हो गया मान लेना। रोल्स अक्सर स्कोप्ड होते हैं (org रोल, team रोल, project रोल)। एक ग्लोबल रोल उन हैक्स को जन्म देता है जैसे "यूजर एक ग्राहक के लिए एडमिन है, पर दूसरे के लिए केवल रीड-ओनली"—जो मल्टी-टेनेन्ट अपेक्षाओं को तोड़ता है और सपोर्ट सिरदर्द बढ़ाता है।
बाद में नुकसान पहुंचाने वाले जाल:
- गलती से क्रॉस-ऑर्ग टीम सदस्यता की अनुमति देना (team_id org A को पॉइंट करता है, membership org B को)
- सदस्यताओं को hard delete करके "किसके पास पिछले हफ्ते एक्सेस था?" का ट्रेल खो देना
- यूनिकनेस नियमों की कमी जिससे यूजर को डुप्लिकेट एक्सेस मिल जाए
- inheritance को चुपचाप स्टैक होने देना (org admin + team member + override) ताकि कोई समझ न पाए कि एक्सेस क्यों है
- "invite accepted" को UI इवेंट मानना, न कि डेटाबेस तथ्य
एक त्वरित उदाहरण: एक कॉन्ट्रैक्टर को org में आमंत्रित किया जाता है, वह Team Sales में जुड़ता है, फिर हटाया जाता है और एक महीने बाद फिर से आमंत्रित किया जाता है। अगर आप पुरानी रो ओवरराइट कर देते हैं तो इतिहास खो जाएगा। अगर आप डुप्लिकेट्स की अनुमति देते हैं तो उनके पास दो सक्रिय सदस्यताएँ हो सकती हैं। स्पष्ट अवस्थाएँ, स्कोप्ड रोल्स, और सही कंस्ट्रेंट्स दोनों समस्याओं को रोकते हैं।
जल्दी जांचें और अगला कदम
कोड लिखने से पहले, अपने मॉडल पर एक तेज़ नज़र डालें और देखें क्या यह कागज़ पर भी अर्थपूर्ण लगता है। एक अच्छा मल्टी-टेनेन्ट एक्सेस मॉडल उबाऊ लगना चाहिए: वही नियम हर जगह लागू हों, और "स्पेशल केस" दुर्लभ हों।
आम गैप पकड़ने के लिए एक तेज़ चेकलिस्ट:
- हर membership बिल्कुल एक user और एक org की ओर इशारा करती है, और डुप्लिकेट रोकने के लिए यूनिक कंस्ट्रेंट हो।
- Invitation, membership, और removal राज्यों को स्पष्ट रखें (nulls से न इम्प्लाय करें), और ट्रांज़िशन सीमित रखें (उदाहरण: expired invite स्वीकार नहीं की जा सकती)।
- रोल्स एक ही जगह स्टोर हों और इफेक्टिव एक्सेस लगातार रूप से गणना की जाए (इनहेरिटेंस नियम शामिल)।
- orgs/teams/users को डिलीट करने से इतिहास न मिटे (ऑडिट ट्रेल के लिए soft delete या आर्काइव फ़ील्ड्स)।
- हर एक्सेस परिवर्तन एक ऑडिट इवेंट छोड़ता है जिसमें actor, target, scope, timestamp, और reason/source होता है।
डिज़ाइन को वास्तविक प्रश्नों से परखें। अगर आप इनका जवाब एक क्वेरी और एक स्पष्ट नियम से नहीं दे पा रहे, तो शायद आपको एक कंस्ट्रेंट या एक अतिरिक्त स्टेट चाहिए:
- क्या होता है अगर किसी यूजर को दो बार इनवाइट किया जाए, फिर ईमेल बदल जाए?
- क्या एक टीम एडमिन किसी org owner को उस टीम से हटा सकता है?
- अगर org रोल सभी टीमों को एक्सेस देता है, क्या कोई टीम उसे ओवरराइड कर सकती है?
- अगर कोई इनवाइट स्वीकार होने के बाद रोल बदल दिया गया हो, तो कौन सा रोल लागू होगा?
- जब सपोर्ट पूछे "किसने एक्सेस हटाया", क्या आप तेजी से साबित कर सकते हैं?
लिखिए कि एडमिन और सपोर्ट स्टाफ क्या समझना चाहिए: सदस्यता अवस्थाएँ (और क्या उन्हें ट्रिगर करता है), कौन इनवाइट/हटाने का अधिकार रखता है, रोल इनहेरिटेंस का सादा अर्थ, और घटना के दौरान ऑडिट इवेंट कहाँ देखें।
सबसे पहले कंस्ट्रेंट लागू करें (uniques, foreign keys, allowed transitions), फिर उनके ऊपर बिजनेस लॉजिक बनाएं ताकि डेटाबेस आपको ईमानदार बने रहने में मदद करे। नीति निर्णयों (inheritance on/off, default roles, invite expiry) को कोड कन्स्टैंट्स की जगह कॉन्फ़िग टेबल्स में रखें।
अगर आप इसे हर चीज़ हाथ से लिखे बिना बनाना चाहते हैं, तो AppMaster (appmaster.io) आपको PostgreSQL में इन तालिकाओं का मॉडल बनाने में मदद कर सकता है और invite और membership ट्रांज़िशन को स्पष्ट बिजनेस प्रोसेसेज़ के रूप में लागू कर सकता है, जबकि उत्पादन तैनाती के लिए वास्तविक स्रोत कोड भी जनरेट करता है।
सामान्य प्रश्न
Use a separate सदस्यता (membership) रिकॉर्ड ताकि रोल और एक्सेस एक org (और वैकल्पिक रूप से एक टीम) से जुड़ा रहे, न कि ग्लोबल यूजर आइडेंटिटी से। इससे वही व्यक्ति एक org में Admin और दूसरे में Viewer हो सकता है बिना किसी हैक के।
अलगा रखें: एक invitation एक ऑफर है जिसमें ईमेल, स्कोप और एक्सपायरी होती है, जबकि एक सदस्यता (membership) का मतलब है कि यूजर के पास वास्तविक एक्सेस है। इससे “गोस्ट मेंबर”, अस्पष्ट स्टेटस और ईमेल बदलने पर सुरक्षा बग टलते हैं।
अधिकांश B2B ऐप्स के लिए active, suspended, और removed जैसे छोटे सेट काफी होते हैं। अगर आप invited स्टेट को सिर्फ invitations टेबल में रखते हैं तो memberships साफ़ रहते हैं: वे वर्तमान या पिछले एक्सेस का प्रतिनिधित्व करते हैं, न कि पेंडिंग एक्सेस का।
org-स्तर और टीम-स्तर के असाइनमेंट को स्कोप के साथ स्टोर करें (org-wide जब team_id NULL हो, टीम-विशिष्ट जब सेट हो)। टीम के लिए एक्सेस चेक करते समय टीम-विशिष्ट असाइनमेंट पाएँ तो उसे प्राथमिकता दें, वरना org-व्यापी असाइनमेंट fallback के रूप में लें।
एक सरल नियम से शुरू करें: org रोल डिफ़ॉल्ट रूप से हर जगह लागू होता है, और टीम रोल केवल तब ओवरराइड करता है जब इसे स्पष्ट रूप से सेट किया गया हो। ओवरराइड कम और स्पष्ट रखें ताकि एक्सेस का कारण आसानी से समझ आ सके।
"एक ही ईमेल के लिए एक पेंडिंग इनवाइट ही हो" यह यूनिक कॉन्स्ट्रेंट के साथ लागू करें और एक स्पष्ट pending/accepted/revoked/expired लाइफसाइकल रखें। री-इनवाइट की जरूरत हो तो मौजूदा पेंडिंग इनवाइट की एक्सपायरी बढ़ाएँ या उसे revoke करके नया टोकन जारी करें।
हर tenant-स्कोप्ड रो में org_id होना चाहिए, और आपके फॉरेन की/कंस्ट्रेंट्स इस बात को रोकें कि अलग-अलग orgs के बीच रेकॉड्स मिसमैच हों (उदाहरण: टीम जिस org से है वही org_id membership में भी होना चाहिए)। इससे ऐप कोड में भूलों का ब्लास्ट रेडियस घटता है।
एक append-only access event लॉग रखें जो रिकॉर्ड करे कि किसने क्या, किस पर, कब और किस स्कोप में किया। जरूरी before/after फील्ड (role, state, team) स्टोर करें ताकि आप भरोसेमंद तरीके से पूछताछों का जवाब दे सकें।
मेम्बरशिप्स और टीम्स के लिए hard delete से बचें; उन्हें ended/disabled मार्क करें ताकि इतिहास क्वेरीयोग्य रहे और फॉरेन की टूटी न हों। इनवाइट्स के लिए भी उन्हें रखना सुरक्षित है (expired भी रखें) ताकि पूरा सुरक्षा ट्रेल रहे, पर कम से कम टोकन पुनः प्रयोग न करें।
अपने हॉट पाथ पर इंडेक्स लगाएँ: org membership चेक के लिए (org_id, user_id), टीम सदस्य सूची के लिए (org_id, team_id), इनवाइट स्वीकार करने के लिए (invite_token), और admin स्क्रीन के लिए (org_id, state)। इंडेक्स उन क्वेरीज़ के अनुरूप रखें जो आप वाकई चलाते हैं।


