Skip to main content
Part 4 gave every user their own boards. That isn’t the same thing as a SaaS, where a company signs up and its people share data nobody outside the company can reach. The difference sounds small and isn’t. Every query has to be scoped, and one missed filter is a data leak rather than a bug. Feather enforces the scoping at three layers, and feather check fails on a tenant-scoped model queried without a tenant filter. You’ll use: TenantScopedMixin, get_current_tenant_id(), require_same_tenant(), domain-based tenant assignment, platform admin, and a separate Attachment model.

Create the project

Multi-tenancy changes the user model and the admin panel, so start fresh again.
There’s no database type prompt this time. Multi-tenant apps require PostgreSQL.

What the scaffold already gives you

Step 1: Tenant-scoped models

The important line is TenantScopedMixin on Kanban. Columns and cards inherit isolation through their relationship to a board, so they don’t carry a tenant_id of their own.
models/kanban.py
models/column.py
models/card.py
models/attachment.py
models/__init__.py
Account and AccountUser both live in models/account.py, and there is no models/account_user.py. Keep them along with Log. The scaffolded services/admin_service.py and seeds.py import all three, and the app won’t start without them.
Part 4 stored a single attachment_path on the card. Here attachments become their own model, which is what lets a card hold several files.

Step 2: The migration

Step 3: Tenant-scoped services

Two functions carry the isolation. get_current_tenant_id() reads the tenant off the signed-in user, and require_same_tenant() raises if a resource belongs to anyone else.
services/kanban_service.py
services/column_service.py
services/card_service.py
move() checks the target column as well as the card. Checking only the card would let someone move their own card into another tenant’s board, which is the kind of gap that looks fine in testing and isn’t.
services/attachment_service.py
services/__init__.py
Isolation flows down the chain: tenant to board to column to card to attachment. Note the storage path carries the tenant ID as well, so two tenants can’t collide in the bucket even by accident.

Steps 4 to 7: Routes, templates and CSS

Delete the scaffolded home page first, since the dashboard replaces it:
The routes follow part 4 closely, with the user-ownership checks swapped for tenant ones, so the services above already show the pattern. The templates and stylesheet are presentation, including a dashboard grid, the board, the drag-drop island and a user menu flyout. Copy them from Steps 4 through 7 of the source tutorial.

Step 8: Test the isolation

1

Sign in as the platform admin

You get an empty dashboard. Create a board, open it, add columns and cards. Then visit Admin, then Tenants.
2

Create a tenant

Name “Acme Corp”, slug “acme”, domain “acme.com”, set to Active.
3

Sign in as an acme.com user in a private window

They land on the pending approval page, because new users need approving.
4

Promote them to tenant admin

As platform admin, go to Admin, Tenants, Acme Corp, Users. Set their role to admin and activate them.
5

Create boards as that user

They’re isolated from every other tenant.
6

Approve a second acme.com user

Sign in as one in another private window, then approve them from Admin, Users as the Acme tenant admin rather than as the platform admin.
Platform admins manage tenants, not users. Approving new signups is each tenant admin’s job, which is what keeps a platform operator out of a customer’s user list.

Prompt Claude

A tenant admin is not a platform admin. An admin at one company must not reach another company’s data, and require_same_tenant() is a hard stop the admin role does not bypass. If an assistant adds an admin shortcut around it, reject the change.

Checkpoint

Sign in as two accounts on different email domains, then confirm:
  • Each is assigned to the tenant matching its domain
  • Neither can see the other’s boards
  • Fetching the other tenant’s board by ID returns 403 rather than data
  • A card cannot be moved into another tenant’s board
  • A tenant admin can manage users inside their tenant only
  • The platform admin can see /admin/tenants and create tenants
  • feather check reports no tenant-isolation errors

What you learned

  • Isolating data per organization instead of per user
  • TenantScopedMixin and the for_tenant() query it adds
  • get_current_tenant_id() at the route layer
  • require_same_tenant() as a hard stop in services
  • Isolation inherited down a hierarchy, from tenant through board to attachment
  • Putting the tenant ID in storage paths as well as the database
  • Assigning users to tenants by email domain
  • Platform admin versus tenant admin, and why approval belongs to the tenant

Next: put it on the internet

A VPS, Docker, automatic TLS, nightly backups and a deploy on every push.