Skip to content

Tenant and Site Isolation

Akash Goswami edited this page Jun 29, 2026 · 1 revision

Tenant and Site Isolation

Four layers ensure Customer A can never see Customer B — and that users only see the sites they're granted.

The 4 layers

  1. Data columns — every pipeline/support/extraction table carries tenant_id + site_id.

  2. JWT identity — the token carries userId, tenantId, role; site_id comes from the request (?site_id= or the X-Site-Id header).

  3. EF Core global query filter — auto-injects WHERE tenant_id = X AND site_id = Y on every query against ITenantScoped entities (Transaction, FileRecord, ActivityLog, InvoiceLineItem).

    Even if a developer forgets a WHERE clause, the global filter protects the data.

  4. Site-access enforcement (FR-5.3)TenantSiteMiddleware checks the requested site_id against the user's UserSiteAccess grants and returns 403 SITE_FORBIDDEN if the user isn't authorized for that site. This closes the within-tenant gap that the tenant filter alone doesn't cover — a user can no longer read another site in their own tenant just by sending its X-Site-Id.

What's intentionally NOT filtered

Identity catalogs (users, sites, user_site_access) and global catalogs (document_types, item_categories, error_catalog) are intentionally not tenant-filtered.

Site selection rules

  • Global tenant scoping is always enforced.
  • Site is optional — supplied via ?site_id= or X-Site-Id.
  • When no site is supplied, services scope by tenant only.
  • When a site is supplied, the middleware validates it against the user's UserSiteAccess grants → 403 SITE_FORBIDDEN if unauthorized.

How to demonstrate it

Seed users have uneven site access on purpose. Example: user.b@acme.com (Chennai only) requesting Mumbai via X-Site-Id is rejected with 403. Full roster + walkthrough in API Reference.

Clone this wiki locally