Lesson 2 / 32

Tenants, Users and Memberships

Model tenants, users and roles correctly so one person can belong to several tenants, and avoid the classic mistake of hanging data off the user.

By · 15 years in web development

Users live inside tenants (usually)

A user is a person who logs in; a tenant is the organisation that owns data. The relationship between them is often many-to-many: a chartered accountant may work in the accounts of several client companies, and a freelancer may join many workspaces. So most B2B SaaS products keep three things: a tenants table, a users table (one identity per person) and a membership table holding (tenant_id, user_id, role). Business data such as invoices or projects carries a tenant_id, not a user_id, because the company owns it even after an employee leaves. When a user signs in, the app either asks which tenant to open or picks a default, and from then on every request runs as that user inside that tenant. Some consumer products are simpler: each user is their own tenant. Decide this early, because changing it later means rewriting authorisation and data ownership.

Modelling memberships

One person, two tenants, a different role in each. Roles belong to the membership, not to the user.

from dataclasses import dataclass, field

@dataclass
class Tenant:
    id: str
    name: str
    plan: str

@dataclass
class User:
    email: str
    memberships: dict = field(default_factory=dict)  # tenant_id -> role

acme = Tenant("t_acme", "Acme Pvt Ltd", "pro")
zen = Tenant("t_zen", "Zen Foods", "free")

priya = User("priya@example.com")
priya.memberships[acme.id] = "admin"
priya.memberships[zen.id] = "viewer"   # one person, two tenants

for tenant_id, role in priya.memberships.items():
    print(f"{priya.email} is {role} in {tenant_id}")

Output:

priya@example.com is admin in t_acme
priya@example.com is viewer in t_zen

Data belongs to the tenant

A frequent design bug is projects.owner_user_id as the only ownership column. When that employee leaves and their account is deleted, the company's projects become orphans. Store tenant_id on every business row, and keep created_by only as an audit fact.

Quick check: In a B2B SaaS, which column should decide who owns an invoice row?

  • The id of the user who created it
  • The id of the user currently logged in
  • The email domain of the creator
  • The tenant_id of the organisation it belongs to
Answer

The tenant_id of the organisation it belongs to — The organisation owns business data. Users come and go, and email domains are not reliable identifiers, so ownership and isolation are keyed on tenant_id.