B2B সংস্থা ও টিমের ডেটাবেস স্কিমা যা স্থিতিশীল থাকে
B2B সংস্থা ও টিমের ডেটাবেস স্কিমা: আমন্ত্রণ, সদস্যপদ অবস্থা, ভূমিকা উত্তরাধিকার এবং অডিট-উপযোগী পরিবর্তনের জন্য একটি ব্যবহারিক রিলেশনাল প্যাটার্ন।

সমস্যাটির সমাধান যা এই স্কিমা প্যাটার্ন দেয়
বেশিরভাগ B2B অ্যাপ আসলে কেবল “ব্যবহারকারী অ্যাকাউন্ট” অ্যাপ নয়। এগুলো শেয়ার করা ওয়ার্কস্পেস যেখানে মানুষ একটি সংস্থায় থাকে, টিমে বিভক্ত, এবং তাদের কাজ অনুসারে ভিন্ন অনুমতি থাকে। সেলস, সাপোর্ট, ফাইন্যান্স, এবং অ্যাডমিনদের ভিন্ন অ্যাক্সেস লাগে, এবং সেই অ্যাক্সেস সময়ের সাথে বদলায়।
খুব সরল মডেল দ্রুত ভেঙে পড়ে। যদি আপনি একটি users টেবিলে শুধু একটি role কলাম রাখেন, তখন আপনি প্রকাশ্যভাবে বলতে পারবেন না “একই ব্যক্তি একটি সংস্থায় Admin, আর অন্য সংস্থায় Viewer।” কন্ট্রাক্টরদের মত সাধারণ কেসও হ্যান্ডল করা মুশকিল—যারা কেবল একটি টিম দেখতে পারবে, বা একজন কর্মী প্রজেক্ট ছেড়ে গেলেও কোম্পানিতে থাকা অবস্থা কী হবে।
আমন্ত্রণ (invites) আরেকটি সাধারণ বাগের উৎস। যদি একটি আমন্ত্রণ কেবল একটি ইমেইল সারি হয়ে যায়, তখন এটা অস্পষ্ট হয়ে পড়ে যে ব্যক্তি কি এখনো সংস্থার “অভ্যন্তরীণ”, তাদের কোন টিমে যোগ দেওয়া হয়েছে, এবং তারা যদি ভিন্ন ইমেইল দিয়ে সাইনআপ করে তাহলে কী হবে। এখানে ছোট অনিয়মগুলো নিরাপত্তা সমস্যায় পরিণত হয়।
এই প্যাটার্ন চারটি লক্ষ্য রাখে:
- নিরাপত্তা: অনুমতিগুলো দৃশ্যমান সদস্যপদ থেকে আসে, অনুমানের উপর নয়।
- স্বচ্ছতা: org, team, এবং roles—প্রতিটি কিছুর একটাই সত্যের উৎস।
- সঙ্গতি: আমন্ত্রণ এবং সদস্যপদ নির্দিষ্ট লাইফসাইকেল অনুসরণ করে।
- ইতিহাস: আপনি ব্যাখ্যা করতে পারবেন কে অ্যাক্সেস দিয়েছে, ভূমিকা বদলিয়েছে, বা কাউকে সরিয়েছে।
প্রতিশ্রুতি হলো এমন একটি রিলেশনাল মডেল যা ফিচার বাড়লেও বোধগম্য থাকে: একটি ব্যবহারকারীর জন্য একাধিক org, প্রতিটি org-এ একাধিক টিম, পূর্বানুমানযোগ্য ভূমিকা উত্তরাধিকার, এবং অডিট-সহায়ক পরিবর্তন। এটি এমন একটি কাঠামো যা আপনি আজই ইমপ্লিমেন্ট করতে পারেন এবং পরে পুনর্লিখন ছাড়াই বাড়াতে পারবেন।
মূল শব্দ: orgs, teams, users, এবং memberships
যদি আপনি এমন একটি স্কিমা চান যা ছয় মাস পরেও পড়ার যোগ্য থাকবে, কিছু শব্দে একমত হোন। বেশিরভাগ বিভ্রান্তি আসে “কে কেউ” এবং “তারা কী করতে পারে” মিশিয়ে ফেলার ফলে।
একটি Organization (org) হল শীর্ষ টেন্যান্ট সীমানা। এটা গ্রাহক বা ব্যবসায়িক অ্যাকাউন্টকে প্রতিনিধিত্ব করে যে ডেটার মালিক। যদি দুইজন ব্যবহারকারী ভিন্ন org-এ থাকে, ডিফল্টভাবে তাদের একে অপরের ডেটা দেখা উচিত নয়। সেই এক নিয়ম অনেক অপ্রত্যাশিত ক্রস-টেন্যান্ট অ্যাক্সেস আটকায়।
একটি Team org-উপরস্থিত একটি ছোট গোষ্ঠী। টিমগুলো প্রকৃত কর্ম ইউনিট মডেল করে: Sales, Support, Finance, বা “Project A।” টিমগুলো org সীমানার পরিবর্তে সেটির ভেতরে থাকে।
একজন User হল পরিচয়—লগইন এবং প্রোফাইল: ইমেইল, নাম, পাসওয়ার্ড বা SSO ID, এবং সম্ভবত MFA সেটিংস। একটি ব্যবহারকারী প্রথমে কোনো অ্যাক্সেস ছাড়াই থাকতে পারে।
একটি Membership হল অ্যাক্সেস রেকর্ড। এটা বলে: “এই ব্যবহারকারী এই org-এ (ঐচ্ছিকভাবে এই টিমেও) এই অবস্থায় এবং এই ভূমিকা নিয়ে আছে।” পরিচয় (User) আলাদা এবং অ্যাক্সেস (Membership) আলাদা রাখলে কন্ট্রাক্টর, অফবোর্ডিং, এবং মাল্টি-অর্গ অ্যাক্সেস মডেল করা সহজ হয়।
কোড এবং UI-তে ব্যাবহার করার জন্য সরল অর্থগুলো:
- Member: একটি সক্রিয় সদস্যপদ সহ ব্যবহারকারী।
- Role: অনুমতিগুলোর একটি নামকৃত বান্ডిల్ (উদাহরণ: Org Admin, Team Manager)।
- Permission: একটি একক অনুমোদিত ক্রিয়া (উদাহরণ: “view invoices”)।
- Tenant boundary: নিয়ম যে ডেটা একটি org-এ সীমাবদ্ধ।
সদস্যপদকে একটি ছোট স্টেট মেশিন হিসেবে বিবেচনা করুন, boolean না। সাধারণ অবস্থা হল invited, active, suspended, এবং removed। এতে আমন্ত্রণ, অনুমোদন, এবং অফবোর্ডিং ধারাবাহিক ও অডিটযোগ্য থাকে।
একক রিলেশনাল মডেল: কোর টেবিল ও সম্পর্ক
একটি ভাল মাল্টি-টেন্যান্ট স্কিমার ধারণা একটাই: “কে কোথায়属” এক জায়গায় রেখে দিন, এবং সব কিছুই সাপোর্টিং টেবিল হিসেবে রাখুন। এইভাবে আপনি মূল প্রশ্নগুলোর উত্তর পেতে পারবেন (কে org-এ আছে, কে টিমে আছে, তাদের কী করতে পারার অনুমতি) অপ্রাসঙ্গিক মডেল জাম্প না করেই।
প্রয়োজনীয় কোর টেবিলগুলো:
- organizations: প্রতিটি গ্রাহক অ্যাকাউন্টের একটি সারি। নাম, স্টেটাস, বিলিং ফিল্ড, এবং একটি অপরিবর্তনীয় id থাকে।
- teams: একটি organization-এর ভেতরের গ্রুপ (Support, Sales, Admin)। সর্বদা একটি organization-এর অন্তর্গত।
- users: প্রতিটি ব্যক্তির জন্য একটি সারি। এটি গ্লোবাল, org-ভিত্তিক নয়।
- memberships: ব্রিজ যা বলে “এই ব্যবহারকারী এই organization-এ অন্তর্ভুক্ত” এবং ঐচ্ছিকভাবে “এই টিমেও।”
- role_grants (বা role_assignments): কোন ভূমিকা একটি membership বা scope-এ পেয়েছে—org লেভেল, team লেভেল, বা উভয়েই।
কী এবং constraints কড়া রাখুন। প্রতিটি টেবিলের জন্য surrogate primary key (UUID বা bigint) ব্যবহার করুন। ফরেন কি যোগ করুন যেমন teams.organization_id -> organizations.id এবং memberships.user_id -> users.id। তারপর কিছু unique constraint যোগ করুন যাতে production-এ duplicate বিরতি আগে থেকেই ধরা পড়ে।
নিয়মগুলো যা বেশিরভাগ ভুল ডেটা শুরুতেই ধরে ফেলে:
- একটি org slug বা এক্সটার্নাল কী:
unique(organizations.slug) - org অনুযায়ী টিম নাম:
unique(teams.organization_id, teams.name) - ডুপ্লিকেট org সদস্যপদ নয়:
unique(memberships.organization_id, memberships.user_id) - ডুপ্লিকেট টিম সদস্যপদ নয় (যদি আপনি আলাদা টিম_memberships মডেল করেন):
unique(team_memberships.team_id, team_memberships.user_id)
নির্ণয় করুন কি অ্যাপেন্ড-ওনলি এবং কি আপডেটযোগ্য। Organizations, teams, এবং users আপডেটযোগ্য। Memberships সাধারণত বর্তমান অবস্থার জন্য আপডেটযোগ্য (active, suspended), কিন্তু পরিবর্তনগুলোর জন্য একটি অ্যাপেন্ড-ওনলি access log লিখে রাখাই ভালো যাতে পরে অডিট করা সহজ হয়।
ধারাবাহিক আমন্ত্রণ ও সদস্যপদ অবস্থা
অ্যাক্সেস পরিষ্কার রাখার সহজ উপায় হল আমন্ত্রণকে একটি আলাদা রেকর্ড হিসেবে রাখা, অর্ধবর্তী সদস্যপদের মতো নয়। একটি membership মানে “এই ব্যবহারকারী বর্তমানে অন্তর্ভুক্ত।” একটি invitation মানে “আমরা অ্যাক্সেস অফার করেছি, কিন্তু তা এখনো বাস্তব নয়।” আলাদা রাখলে ghost members, অসম্পূর্ণ অনুমতি, এবং “কে আমন্ত্রণ দিয়েছিল?”-এর রহস্য এড়ানো যায়।
একটি সরল, বিশ্বাস যোগ্য স্টেট মডেল
Memberships-এর জন্য এমন একটি ছোট স্টেট সেট ব্যবহার করুন যা যেকোনো জনের কাছে ব্যাখ্যা করা যায়:
active: ব্যবহারকারী org (এবং তারা যে টিমের সদস্য সেখানে) অ্যাক্সেস পেতে পারেsuspended: সাময়িকভাবে ব্লক করা, কিন্তু ইতিহাস অক্ষুণ্ণ থাকেremoved: আর সদস্য নয়, audit ও রিপোর্টিংয়ের জন্য রাখা
অনেকে সদস্যপদে “invited” স্টেট না রেখে আমন্ত্রণকে কেবল invitations টেবিলে রাখে। তা বেশ পরিষ্কার থাকে: membership সারি কেবল তাদের জন্য থাকে যাদের সত্যিই অ্যাক্সেস আছে (active), বা যাদের পূর্বে ছিল (suspended/removed)।
অ্যাকাউন্ট না থাকলে ইমেইল-ভিত্তিক আমন্ত্রণ
B2B অ্যাপগুলো প্রায়ই ইমেইল দিয়ে আমন্ত্রণ দেয় যখন কোন ব্যবহারকারী অ্যাকাউন্ট এখনও নেই। আমন্ত্রণ রেকর্ডে ইমেইল সংরক্ষণ করুন, কোথায় আমন্ত্রণ প্রযোজ্য (org বা team), লক্ষ্যমাত্রা ভূমিকা, এবং কে পাঠিয়েছে তা রাখুন। যদি পরে সেই ব্যক্তি একই ইমেইল দিয়ে সাইন আপ করে, pending invitations ম্যাচ করে তাদের গ্রহণ করতে দিন।
একটি আমন্ত্রণ গ্রহণ হলে, এটিকে একটি ট্রানজাকশনে হ্যান্ডল করুন: আমন্ত্রণকে accepted চিহ্নিত করুন, membership তৈরি করুন, এবং এক অডিট এন্ট্রি লিখুন (কে গ্রহণ করলো, কখন, এবং কোন ইমেইল ব্যবহার করা হয়েছিল)।
স্পষ্ট আমন্ত্রণ শেষ-অবস্থাগুলো নির্ধারণ করুন:
expired: মেয়াদপেরিয়ে গেছে এবং গ্রহণ করা যাবে নাrevoked: অ্যাডমিন দ্বারা বাতিল করা হয়েছে এবং গ্রহণ করা যাবে নাaccepted: membership-এ রূপান্তরিত হয়েছে
ডুপ্লিকেট আমন্ত্রণ প্রতিরোধ করুন “প্রতি org বা টিম প্রতি ইমেইল কেবল একটি pending invite” বাধ্যবাধকতা দিয়ে। যদি আপনি re-invite সমর্থন করেন, তাহলে অথবা বিদ্যমান pending invite-এর মেয়াদ বাড়ান অথবা পুরনোটি revoke করে একটি নতুন টোকেন ইস্যু করুন।
ভূমিকা ও উত্তরাধিকার যাতে অ্যাক্সেস বিভ্রান্তিকর না হয়
বেশিরভাগ B2B অ্যাপে দরকার হয় দুটি লেভেলের অ্যাক্সেস: একটি হলো সংস্থার সার্বিক যা কেউ কী করতে পারে, এবং টিম-স্তর যে টিমের ভেতরে কী করতে পারে। এগুলোকে এক role ফিল্ডে মিশিয়ে ফেললে অ্যাপগুলো অনিশ্চিত বোধ করে।
Org-লেভেল ভূমিকা প্রশ্নগুলোর উত্তর দেয়: এই ব্যক্তি কি বিলিং পরিচালনা করতে পারে, মানুষ আমন্ত্রণ করতে পারে, বা সব টিম দেখতে পারে? টিম-লেভেল ভূমিকা জানায়: তারা Team A-তে আইটেম সম্পাদনা করতে পারে কি না, Team B-এ অনুমোদন করতে পারে কি না, বা কেবল দেখতেই পারে কি না?
ভূমিকা উত্তরাধিকার এমনভাবে সহজ থাকা উচিত যে এক নিয়ম মেনে চলে: org ভূমিকা সব জায়গায় প্রযোজ্য যতক্ষণ না কোনো টিম স্পষ্টভাবে বাইরে বলে। এতে আচরণ পূর্বানুমানযোগ্য থাকে এবং ডুপ্লিকেট ডেটা কমে।
এটি মডেল করার একটি পরিষ্কার উপায় হলো role assignments-কে একটি scope সহ স্টোর করা:
role_assignments:user_id,org_id, optionalteam_id(NULL মানে org-ব্যাপী),role_id,created_at,created_by
আপনি যদি “একটি scope-এ এক রোল” চান, তাহলে (user_id, org_id, team_id)-এর ওপর unique constraint যোগ করুন।
তখন কোনো টিমের কার্যকর অ্যাক্সেস নির্ধারণ হবে:
-
টিম-নির্দিষ্ট অ্যাসাইনমেন্ট দেখুন (
team_id = X)—যদি আছে, সেটি ব্যবহার করুন। -
অন্যথায় org-ব্যাপী অ্যাসাইনমেন্টে fallback করুন (
team_id IS NULL)।
least-privilege ডিফল্ট রাখার জন্য একটি ন্যূনতম org রোল বাছুন (প্রায়ই “Member”) এবং এটিতে লুকানো অ্যাডমিন ক্ষমতা দেবেন না। নতুন ব্যবহারকারীরাই গোপনে টিম অ্যাক্সেস পায় না যদি না পণ্যটি সত্যিই সেটা চায়। যদি আপনি অটো-গ্র্যান্ট করেন, সেটা স্পষ্ট টিম সদস্যপদ তৈরি করে করুন, org রোল চুপচাপ বিস্তৃত না করে।
ওভাররাইড অবশ্যই বিরল এবং স্পষ্ট হওয়া উচিত। উদাহরণ: মারিয়া org-এ “Manager” (invites দিতে পারে, রিপোর্ট দেখতে পারে), কিন্তু Finance টিমে তিনি “Viewer” হবেন। আপনি মারিয়ার জন্য একটি org-ব্যাপী অ্যাসাইনমেন্ট রাখবেন এবং Finance এর জন্য একটি টিম-স্কোপড ওভাররাইড রাখবেন। কোনো অনুমতি কপিং নেই, এবং ব্যতিক্রম দৃশ্যমান।
ভূমিকার নাম সাধারণ প্যাটার্নের জন্য ভাল কাজ করে। এক্সপ্লিসিট পারমিশন কেবল তখনই ব্যবহার করুন যখন প্রকৃত ওয়ান-অফ প্রয়োজন (যেমন “export করতে পারবে কিন্তু edit করতে পারবে না”), অথবা যখন কমপ্লায়েন্স স্পষ্ট অনুমোদিত ক্রিয়ার তালিকা চায়। তাও scope ধারণা একই রাখুন যাতে মনস্তত্ত্ব সহজ থাকে।
অডিট-বন্ধু পরিবর্তন: কে অ্যাক্সেস বদল করেছে সেটা ট্র্যাক করা
আপনি যদি কেবল membership সারিতে মাত্র বর্তমান রোল রাখেন, আপনি গল্প হারান। কেউ যদি জানতে চায়, “গত মঙ্গলবার কে Alex-কে admin করেছে?” তেমন হলে নির্ভরযোগ্য উত্তর নেই। আপনাকে পরিবর্তনের ইতিহাস রাখতে হবে, কেবল বর্তমান অবস্থা নয়।
সরল পদ্ধতি হলো এক নিবেদিত audit log টেবিল যাতে access ইভেন্ট রেকর্ড করা হয়। এটিকে একটি অ্যাপেন্ড-ওনলি জার্নাল হিসেবে ব্যবহার করুন: পুরোনো audit সারি কখনো এডিট করবেন না; কেবল নতুনগুলো যোগ করুন।
একটি ব্যবহারিক audit টেবিলে সাধারণত থাকে:
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) ব্যবহার করতে পারেন, কিন্তু পে-লোড ছোট এবং সঙ্গতিপূর্ণ রাখুন।
Memberships এবং teams-এর জন্য soft delete সাধারণত hard delete-এর তুলনায় ভাল। সারি মুছে ফেলার বদলে এটিকে disabled হিসেবে মার্ক করুন deleted_at ও deleted_by মত ফিল্ড দিয়ে। এতে ফরেনকি অক্ষত থাকে এবং অতীত অ্যাক্সেস বোঝা সহজ হয়। Hard delete কেবল অস্থায়ী রেকর্ডের (যেমন expired invites) জন্য মানতে পারেন, কিন্তু কেবল যদি আপনি নিশ্চিত হন যে পরে তাদের দরকার হবে না।
এর মাধ্যমে আপনি দ্রুত সাধারণ কমপ্লায়েন্স প্রশ্নগুলোর উত্তর দিতে পারবেন:
- কে অ্যাক্সেস দিয়েছে বা সরিয়েছে, এবং কখন?
- ঠিক কী পরিবর্তিত হয়েছে (role, team, state)?
- অ্যাক্সেস কি স্বাভাবিক অফবোর্ডিং ফ্লোর অংশ হিসেবে সরানো হয়েছিল?
ধাপ ধরে: রিলেশনাল ডাটাবেসে স্কিমা ডিজাইন করা
সহজে শুরু করুন: একজন যে কোথায়属 তা বলার এক জায়গা, এবং কেন তা লিখুন। ছোট ছোট ধাপে তৈরি করুন, এবং নিয়ম যোগ করুন যাতে ডেটা “প্রায় সঠিক” হয়ে না যায়।
PostgreSQL ও অন্যান্য রিলেশনাল ডাটাবেসে কার্যকর একটি প্র্যাকটিক্যাল অর্ডার:
-
organizationsএবংteamsতৈরি করুন, প্রতিটিতে একটি স্থায়ী primary key (UUID বা bigint)।teams.organization_idকে ফরেন কি হিসেবে যোগ করুন, এবং আগে ঠিক করুন টিম নাম org-এর ভিতরে ইউনিক হওয়া উচিত কি না। -
users-কে সদস্যপদ থেকে আলাদা রাখুন। পরিচয় ফিল্ডগুলোusers-এ (email, status, created_at) রাখুন। “কোনো org/team-এ 属”membershipsটেবিলে রাখুন যার মধ্যেuser_id,organization_id, ঐচ্ছিকteam_id, এবং একটিstateকলাম (active, suspended, removed) থাকবে। -
invitationsআলাদা টেবিল হিসেবে যোগ করুন, membersip-এর উপর একটি কলাম না।organization_id, ঐচ্ছিকteam_id,email,token,expires_at, এবংaccepted_atসংরক্ষণ করুন। “প্রতি org + ইমেইল + টিম কেবল এক ওপেন ইনভাইট” বজায় রাখতে uniqueness চাপান যাতে ডুপ্লিকেট না তৈরি হয়। -
roles স্পষ্ট টেবিল দিয়ে মডেল করুন। সহজ উপায় হলো
roles(admin, member ইত্যাদি) এবংrole_assignmentsযা org scope (noteam_id) বা team scope (team_idসেট) নির্দেশ করে। উত্তরাধিকারের নিয়ম সঙ্গতিপূর্ণ রাখুন এবং টেস্ট করুন। -
প্রথম দিন থেকেই audit trail যোগ করুন।
access_eventsটেবিল ব্যবহার করুন যাতেactor_user_id,target_user_id(অথবা invites-এর জন্য email),action(invite_sent, role_changed, removed),scope(org/team), এবংcreated_atথাকে।
এই টেবিলগুলো তৈরি হলে কয়েকটি সাধারণ অ্যাডমিন কুইরি চালান বাস্তবতা যাচাই করার জন্য: “কারা org-ব্যাপী অ্যাক্সেস পায়?”, “কোন টিমগুলোর কোনো অ্যাডমিন নেই?”, এবং “কোন invites মেয়াদোত্তীর্ণ কিন্তু এখনও ওপেন?” এই প্রশ্নগুলো সাধারণত অনুশীলনের শুরুর দিকে অনুপস্থিত constraints আবিষ্কার করায়।
ডেটা নোংরা হওয়া প্রতিরোধকারী নিয়ম ও constraints
একটি স্কিমা তখনই সুসংগঠিত থাকে যখন ডাটাবেস, কেবল আপনার কোড নয়, টেন্যান্ট সীমানা লঙ্ঘন প্রতিরোধ করে। সোজা নিয়ম হলো: প্রতিটি tenant-scoped টেবিলে org_id থাকে, এবং প্রতিটি লুকআপ তাতে অন্তর্ভুক্ত। এমনকি যদি কেউ অ্যাপে ফিল্টার ভুলে যান, ডাটাবেস ক্রস-org সংযোগকে রোধ করবে।
ডেটা পরিষ্কার রাখার গার্ডরেইল
শুরুতে ফরেনকি যোগ করুন যা সবসময় একই org-এর মধ্যে ইঙ্গিত করে। উদাহরণস্বরূপ, যদি আপনি team_memberships আলাদা রাখেন, একটি team_memberships সারি team_id এবং user_id রেফার করে, কিন্তু org_idও বহন করা উচিত। কম্পোজিট কী দিয়ে আপনি নিশ্চিত করতে পারেন যে রেফারেন্স করা টিমটি একই org-র অন্তর্গত।
বেশিরভাগ সাধারণ সমস্যাগুলো প্রতিরোধকারী constraints:
- প্রতি user প্রতি org এক active সদস্যপদ:
(org_id, user_id)-এ unique (যেখানে supported হলে partial condition ব্যবহার করুন)। - প্রতি ইমেইল প্রতি org/টিম কেবল একটি pending invite:
(org_id, team_id, email)-এ unique যেখানেstate = 'pending'। - invite tokens গ্লোবালি ইউনিক এবং পুনঃব্যবহার হবে না:
unique(invite_token)। - টিম ঠিক এক org-কে অন্তর্ভুক্ত করে:
teams.org_idNOT NULL এবংorgs(id)-এ ফরেন কি। - সদস্যপদ মুছুন না—তার বদলে
ended_at(এবংended_by) রাখুন যাতে অডিট ইতিহাস রক্ষা পায়।
যে লুকআপগুলোই আপনি আসলে করবেন, সেগুলোর জন্য ইনডেক্সিং
আপনার অ্যাপ যে কুইরিগুলো নিয়মিত চালায় সেগুলো ইনডেক্স করুন:
(org_id, user_id)প্রশ্নের জন্য “এই ব্যবহারকারী কোন কোন org-এ আছে?”(org_id, team_id)প্রশ্নের জন্য “এই টিমের সদস্যদের তালিকা”(invite_token)প্রশ্নের জন্য “ইনভাইট গ্রহণ”(org_id, state)প্রশ্নের জন্য “pending invites” এবং “active members”
org নাম পরিবর্তনযোগ্য রাখুন। সর্বত্র immutable orgs.id ব্যবহার করুন, এবং orgs.name (এবং slug) কেবল এডিটেবল ফিল্ড হিসেবে রাখুন। নাম পরিবর্তন করলে একটি সারি আপডেট হবে।
টিম একটি org থেকে অন্য org-এ স্থানান্তর সাধারণত নীতি-ভিত্তিক সিদ্ধান্ত। সবচেয়ে নিরাপদ বিকল্প হলো এটি নিষিদ্ধ করা (অথবা টিম ক্লোন করা) কারণ memberships, roles, এবং অডিট ইতিহাস org-স্কোপড। যদি আপনাকে মুভ করতে হবে, একক ট্রানজাকশনে করুন এবং সব child সারি আপডেট করুন যেগুলো org_id বহন করে।
ব্যবহারকারী চলে গেলে orphan রেকর্ড প্রতিরোধ করতে, hard deletes এড়িয়ে চলুন। ব্যবহারকারীকে অক্ষম করুন, তাদের memberships শেষ করুন, এবং parent সারিগুলোর উপর deletes সীমাবদ্ধ করুন (ON DELETE RESTRICT) যদি না আপনি সত্যিই cascading removal চান।
উদাহরণ দৃশ্য: একটি org, দুই টিম, নিরাপদভাবে অ্যাক্সেস পরিবর্তন
ধরা যাক Northwind Co নামে একটি কোম্পানি আছে যার একটি org এবং দুইটি টিম: Sales এবং Support। তারা একজন কন্ট্রাক্টর Mia-কে Support টিকিটে এক মাসের জন্য নেয়। মডেলটি এখানে পূর্বানুমানযোগ্য থাকা উচিত: এক ব্যক্তি, এক org সদস্যপদ, ঐচ্ছিক টিম সদস্যপদ, এবং স্পষ্ট স্টেট।
এক org অ্যাডমিন (Ava) Mia-কে ইমেইলে আমন্ত্রণ করে। সিস্টেম একটি invitation সারি তৈরি করে org-সংযুক্ত, pending স্টেট এবং একটি মেয়াদসহ। কিছুই বদলে যায় না—তাই কোনও “অর্ধ সদস্য” নেই যার অস্পষ্ট অ্যাক্সেস।
Mia যখন গ্রহণ করে, invitation accepted চিহ্নিত হয়, এবং একটি org membership সারি active অবস্থায় তৈরি হয়। Ava Mia-র org রোল member (Admin নয়) সেট করে। তারপর Ava Support টিম সদস্যপদ যোগ করে এবং একটি টিম রোল যেমন support_agent অ্যাসাইন করে।
এখন একটি টুইস্ট যোগ করুন: Ben একজন ফুল-টাইম কর্মী এবং org রোল হিসেবে admin, কিন্তু তিনি Support ডেটা দেখতে পারা উচিৎ নয়। আপনি সেটি একটি টিম-লেভেল ওভাররাইড দিয়ে হ্যান্ডেল করতে পারেন যা স্পষ্টভাবে তার Support টিম রোল কমিয়ে দেয় যখন org-ব্যাপী ক্ষমতাগুলো রয়ে যায়।
এক সপ্তাহ পরে, Mia নীতি ভাঙায় এবং suspended করা হয়। সারি মুছার বদলে Ava Mia-র org membership স্টেট suspended করে। টিম সদস্যপদ স্থায়ী থাকতে পারে কিন্তু কার্যকর হবে না কারণ org membership active নয়।
অডিট ইতিহাস পরিষ্কার থাকে কারণ প্রতিটি পরিবর্তন একটি ইভেন্ট:
- Ava Mia-কে আমন্ত্রণ পাঠায় (কে, কী, কখন)
- Mia আমন্ত্রণ গ্রহণ করে
- Ava Mia-কে Support-এ যোগ করে এবং
support_agentঅ্যাসাইন করে - Ava Ben-এর Support ওভাররাইড সেট করে
- Ava Mia-কে suspended করে
এই মডেলের সাথে UI একটি স্পষ্ট অ্যাক্সেস সামারি দেখাতে পারে: org স্টাটাস (active বা suspended), org রোল, টিম তালিকা রোল ও ওভাররাইডসহ, এবং “সাম্প্রতিক অ্যাক্সেস পরিবর্তন” ফিড যা ব্যাখ্যা করে কেন কেউ Sales বা Support দেখতে পারে বা পারে না।
সাধারণ ভুল এবং ফাঁদ যা এড়ানো উচিত
বেশিরভাগ অ্যাক্সেস বাগ আসে “প্রায় সঠিক” ডেটা মডেল থেকে। স্কিমা প্রথমে ঠিক মনে হয়, তারপর এজ কেস যোগ হয়ে যায়: re-invites, টিম মুভ, রোল পরিবর্তন, এবং অফবোর্ডিং।
একটি সাধারণ ফাঁদ হল আমন্ত্রণ এবং সদস্যপদ এক রেকর্ডে মিশিয়ে ফেলা। যদি আপনি “invited” ও “active” একই রেকর্ডে স্পষ্ট অর্থ না রেখে রাখেন, আপনি এমন প্রশ্ন জিজ্ঞেস করতে শুরু করবেন যেগুলোর উত্তর অসম্ভব: “যদি কেউ কখনো গ্রহণ না করে, তারা কি সদস্য?” আমন্ত্রণ এবং সদস্যপদ আলাদা রাখুন, বা স্টেট মেশিনটি স্পষ্ট ও সঙ্গতিপূর্ণ করুন।
আরেকটি বারবারের ভুল হল user টেবিলে একটি single role কলাম রেখে সব সমাধান করা। রোল প্রায়ই scoped (org role, team role, project role)। একটি গ্লোবাল রোল বাধ্যতামূলকভাবে হ্যাক আনতে বাধ্য করে যেমন “ব্যবহারকারী এক কাস্টমারে admin, আর অন্য কাস্টমারে read-only”—এটি মাল্টি-টেন্যান্ট প্রত্যাশা ভেঙে দেয় এবং সাপোর্ট মাথাব্যথা বাড়ায়।
যেসব ফাঁদ পরে বেশি কষ্ট দেয়:
- দুর্ঘটনবশত ক্রস-org টিম সদস্যপদ অনুমতি (team_id org A-কে ইঙ্গিত করে, membership org B-কে)
- memberships hard delete করে “গত সপ্তাহে কারা অ্যাক্সেস পেত?” ট্রেইল হারানো
- uniqueness নিয়ম না থাকার কারণে ব্যবহারকারী একাধিক লাইন দ্বারা ডুপ্লিকেট অ্যাক্সেস পাওয়া
- উত্তরাধিকার নীরবে স্ট্যাকিং (org admin + team member + override) যাতে কেউ ব্যাখ্যা করতে পারে না কেন অ্যাক্সেস আছে
- “invite accepted” কে UI ইভেন্ট হিসেবে বিবেচনা করা, ডাটাবেস ফ্যাক্ট হিসেবে না রাখা
একটি দ্রুত উদাহরণ: একজন কন্ট্রাক্টরকে org-এ আমন্ত্রণ করা হয়, তিনি Team Sales-এ যোগ দেন, পরে সরিয়ে দেওয়া হয় এবং এক মাস পরে পুনরায় আমন্ত্রণ করা হয়। যদি আপনি পুরনো সারিটি ওভাররাইট করেন, ইতিহাস হারবে। যদি আপনি ডুপ্লিকেট অনুমতি দেয়, তারা একসাথে দুইটি active membership পেতে পারে। স্পষ্ট স্টেট, scoped roles, এবং সঠিক constraints উভয়ই প্রতিকূলতা প্রতিরোধ করে।
দ্রুত চেকলিস্ট এবং পরবর্তী ধাপ আপনার অ্যাপে এটি তৈরি করার জন্য
কোড করার আগে, আপনার মডেলটি কাগজে দেখে নিন এবং দেখুন এটা কি এখনও বোধগম্য। একটি ভাল মাল্টি-টেন্যান্ট অ্যাক্সেস মডেল বোরিং লাগা উচিত: একই নিয়ম সব জায়গায় প্রযোজ্য, এবং “স্পেশাল কেস” বিরল।
একটি দ্রুত চেকলিস্ট সাধারণ ফাঁক ধরতে:
- প্রতিটি membership ঠিক একটি user এবং একটি org-কে পয়েন্ট করে, এবং duplicate প্রতিরোধে unique constraint আছে।
- Invitation, membership, এবং removal state গুলো স্পষ্ট (null দ্বারা ইঙ্গিত নয়), এবং ট্রানজিশনগুলো সীমিত (উদাহরণ: expired invite গ্রহণ করা যাবে না)।
- Roles এক স্থানে রাখা এবং কার্যকর অ্যাক্সেস ধারাবাহিকভাবে ক্যালকুলেট করা (উত্তরাধিকারের নিয়মসহ, যদি ব্যবহৃত হয়)।
- Org/teams/users মুছলে ইতিহাস মুছে যায় না (audit ট্রেইল বা soft delete রাখুন)।
- প্রতিটি অ্যাক্সেস পরিবর্তন একটি audit ইভেন্ট তৈরি করে: actor, target, scope, timestamp, এবং reason/source।
ডিজাইনকে বাস্তব প্রশ্ন দিয়ে চাপ দিন। যদি আপনি এগুলো একটি কুইরি এবং একটি স্পষ্ট নিয়ম দিয়ে উত্তর দিতে না পারেন, সম্ভবত আপনাকে constraint বা অতিরিক্ত স্টেট যোগ করতে হবে:
- যদি একজন ব্যবহারকারীকে দুবার আমন্ত্রণ করা হয়, তারপর ইমেইল পরিবর্তন হলে কী হয়?
- কি একটি টিম অ্যাডমিন কোনো org owner-কে সেই টিম থেকে সরাতে পারে?
- যদি একটি org রোল সব টিমে অ্যাক্সেস দেয়, এক টিম কি সেটা ওভাররাইড করবে?
- যদি একটি invite গ্রহণের পরে রোল বদলে যায়, কোন রোল প্রযোজ্য হবে?
- যখন সাপোর্ট জিজ্ঞাসা করে “কে অ্যাক্সেস সরিয়েছিল,” আপনি কি দ্রুত প্রমাণ করতে পারবেন?
অ্যাডমিন এবং সাপোর্ট স্টাফ কি বুঝবে তা লিখে রাখুন: সদস্যপদ স্টেট (এবং কী ট্রিগার করে), কে আমন্ত্রণ/সরাতে পারে, ভূমিকা উত্তরাধিকারের সরল ভাষ্য, এবং ঘটনার সময় অডিট ইভেন্ট কোথায় খুঁজবে।
প্রথমে constraints (uniques, foreign keys, allowed transitions) ইমপ্লিমেন্ট করুন, তারপর ব্যবসায়িক লজিক গড়ে তুলুন যাতে ডাটাবেস আপনাকে সৎ থাকতে সাহায্য করে। policy সিদ্ধান্তগুলো (উত্তরাধিকার অন/অফ, ডিফল্ট রোল, invite expiry) কোড কনস্ট্যান্ট না রেখে কনফিগ টেবিলে রাখুন যেন পরবর্তীতে পরিবর্তন সহজ হয়।
যদি আপনি সব ব্যাকএন্ড ও অ্যাডমিন স্ক্রিন হাতে লিখে না করেই এটি গড়তে চান, AppMaster (appmaster.io) আপনাকে PostgreSQL-এ এই টেবিলগুলো মডেল করতে এবং invite ও membership ট্রানজিশনগুলোকে স্পষ্ট ব্যবসায়িক প্রসেস হিসেবে তৈরি করতে সাহায্য করতে পারে, এবং সঙ্গেই প্রোডাকশনের জন্য বাস্তব সোর্স কোড জেনারেট করতে পারে।
প্রশ্নোত্তর
Use a separate membership record so roles and access are tied to an org (and optionally a team), not to the global user identity. That lets the same person be an Admin in one org and a Viewer in another without hacks.
Keep them separate: an invitation is an offer with an email, scope, and expiry, while a membership means the user actually has access. This avoids “ghost members,” unclear status, and security bugs when emails change.
A small set like active, suspended, and removed is enough for most B2B apps. If you keep “invited” only in the invitations table, memberships stay unambiguous: they represent current or past access, not pending access.
Store org roles and team roles as assignments with a scope (org-wide when team_id is null, team-specific when it’s set). When checking access for a team, prefer the team-specific assignment if it exists, otherwise fall back to the org-wide one.
Start with one predictable rule: org roles apply everywhere by default, and team roles override only when explicitly set. Keep overrides rare and visible so people can explain access without guessing.
Enforce “only one pending invite per org/team per email” with a unique constraint and a clear pending/accepted/revoked/expired lifecycle. If you need re-invites, update the existing pending invite or revoke it before issuing a new token.
Every tenant-scoped row should carry org_id, and your foreign keys/constraints should prevent mixing orgs (for example, a team referenced by a membership must belong to the same org). This reduces the blast radius of missing filters in application code.
Keep an append-only access event log that records who did what, to whom, when, and at what scope (org or team). Record the key before/after fields (role, state, team) so you can answer “who granted admin last Tuesday?” reliably.
Avoid hard deletes for memberships and teams; mark them ended/disabled so history remains queryable and foreign keys don’t break. For invites, you can keep them too (even if expired) if you want a full security trail, but at minimum don’t reuse tokens.
Index your hot paths: (org_id, user_id) for org membership checks, (org_id, team_id) for team member lists, (invite_token) for invite acceptance, and (org_id, state) for admin screens like “active members” or “pending invites.” Indexes should reflect your real queries, not every column.


