TenantScopedMixin — multi-tenant SaaS pattern

Two seeded tenants. Switch via the URL. Querysets auto-scope; writes stamp the tenant; IDs from another tenant 404 instead of leaking. The three leak vectors closed.

Active tenant: globex

You're seeing 3 orders for globex (out of 6 total in the database). The other 3 rows belong to acme (or to legacy unscoped rows) and are invisible from here.

# Customer Total Status
6 Globex Field Ops $199.00 pending
5 Globex Lab $149.00 pending
4 Globex Tower $99.00 pending

The three leak-vector defenses

  1. List queries always include .filter(tenant=self.tenant) — a globex user never sees an acme order in any list.
  2. Detail/edit lookups by ID also filter by tenant — prevents IDOR via guessable PKs across tenant boundaries (a globex user passing an acme order's ID gets 404, not the row).
  3. Write paths stamp tenant=self.tenant on creation — a tenant can't sneak rows into another's namespace by spoofing form fields.

Production: TenantScopedMixin handles all three automatically

from djust.tenants import TenantScopedMixin

class OrderList(TenantScopedMixin, LiveView):
    model = Order

    def mount(self, request, **kwargs):
        # self.tenant is set automatically by TenantMiddleware
        # get_tenant_queryset() always filters by self.tenant.id
        self.orders = self.get_tenant_queryset()

    @event_handler()
    def add_order(self, customer, total, **kwargs):
        # Always stamp the tenant on writes — closing the
        # third leak vector (cross-tenant create).
        Order.objects.create(
            customer=customer, total=total,
            tenant=self.tenant,
        )

This demo manually filters via ?tenant= query param to keep the scaffold lightweight. Production setup uses DJUST_CONFIG['TENANT_RESOLVER'] (subdomain / path / header / session) plus TenantMiddleware. See docs.djust.org/api/tenants/.