Skip to main content
Part 4 gave every user their own boards. A SaaS works differently: a company signs up, its people share data, and nobody outside the company can reach it. Making that change means scoping every query, and a missed filter exposes one customer’s data to another. Read what comes back from this part carefully. feather check helps by failing on a tenant-scoped model queried without a tenant filter, but it checks the query shape rather than your intent.

What you’ll have

  • Users assigned to a company automatically, by email domain
  • Boards, columns, cards and attachments isolated per company
  • A tenant admin who manages users inside their own company
  • A platform admin who creates companies but doesn’t touch their user lists
  • Files in cloud storage separated by tenant as well

Which parts of Feather this uses

Multi-tenancy

TenantScopedMixin, get_current_tenant_id() and require_same_tenant().

The two-axis model

Tenant role and platform authority are separate, and admin doesn’t cross companies.

feather check

The tenant-isolation rule turns a missed filter into a failed build.

File storage

Tenant IDs go in the storage path, so buckets are isolated too.

Step 1: Scaffold

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.
The first migration is already generated and applied. If Postgres was not running while the app was created, run feather db migrate -m "Initial migration" and feather db upgrade first. The scaffold is meaningfully different from part 4’s:

Step 2: Run the prompt

The prompt
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 doesn’t bypass. If the assistant adds an admin shortcut around it, reject the change.

Step 3: Test it

Clicking through your own account proves nothing here. You need two, on different email domains.
1

Sign in as the platform admin

Create a board, open it, add columns and cards. Then go to 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.
4

Promote them to tenant admin

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

Create boards as the Acme user

Then confirm the platform admin’s dashboard doesn’t show them, and the Acme user’s doesn’t show the platform admin’s.
6

Try to cross the line

Copy a board ID from one account and open /kanban/<that-id> from the other. You want a 403, not a board.
Zero tenant-isolation errors is the result that matters here.

What the framework did

Isolation is declarative at the model. TenantScopedMixin added tenant_id and the for_tenant() query. Columns and cards don’t carry one, because their owner is reachable through the board, and duplicating it would mean two sources of truth that can disagree. require_same_tenant() raises rather than filters. A filter that silently returns nothing looks identical to a genuinely empty result, which hides the bug. An exception becomes a 403 you can see in the logs. See Multi-tenancy. The linter is the backstop. feather check’s tenant-isolation rule catches a query on a tenant-scoped model with no tenant filter, so the mistake fails CI rather than shipping. See feather check. Approval belongs to the tenant. Platform admins create companies. Each company’s admin approves its own users. That’s what keeps a platform operator out of a customer’s user list, and it’s a product decision the framework encodes rather than leaves to you.

Next: put it on the internet

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