Developer Guide / Site Isolation & Multi-Tenancy

Site Isolation & Multi-Tenancy

How one deployment serves many isolated sites, the SITE_ADMIN role, and the gateway-enforced site context.

A single Magic Varsh deployment is multi-tenant at the site level. Every administrable resource carries a nullable site_id, and requests are bound to a site whose scope is resolved and enforced at the API gateway — not trusted from the client.

The site-context contract

Authentication issues a JWT that now carries a siteId claim. On every request the gateway validates the JWT and injects two trusted headers that downstream services rely on, overwriting or stripping any client-supplied values:

  • X-Site-Id — the authoritative site for this request (absent = global).
  • X-Site-Scope — SITE (filter to exactly this site) or GLOBAL (cross-site / platform view).
PrincipalJWT siteIdEffective X-Site-Id
Site-bound user / SITE_ADMINSforced to S — inbound value discarded
Global ADMIN targeting a site—the site it explicitly targets
Global ADMIN, no target—absent → all sites (GLOBAL)
Other global user—none (a forged header is dropped)
i
Because the gateway forces X-Site-Id for site-bound principals, a Site Administrator physically cannot address another site by tampering with headers.

The SITE_ADMIN role

ROLE_SITE_ADMIN is a global, reserved role (like ROLE_ADMIN and ROLE_GUEST). The binding to a specific site comes from the user's site_id, not from the role. So a user with ROLE_SITE_ADMIN and site_id = S administers only site S. Guards prevent a site admin from assigning reserved global roles or touching out-of-site users and roles (403).

What is scoped per site

Pages, page layouts, portlets, assets, web content, navigation, dynamic forms, email templates, third-party/SOAP integration components, workflows, users, roles, trash, list-of-values (with a shared global tier), labels, settings (SMTP / SMS / SSL / media / language), auth providers (many per site), the scheduler (site jobs vs system jobs), and audit logs are all site-scoped. List of Values and Labels keep a shared global tier that a site can override.

Enforcing scope in a service

A thread-local SiteContext in magicvarsh-common is populated from the injected headers, so services filter and stamp site_id uniformly:

// controller
@GetMapping
List<Foo> list(@RequestHeader(value = "X-Site-Id", required = false) Long siteId) {
    return siteId != null ? repo.findBySiteId(siteId) : repo.findAll(); // GLOBAL admin sees all
}

// service layer
Long site = SiteContext.siteId();      // authoritative, gateway-resolved
boolean global = SiteContext.isGlobal();
!
Never trust a siteId that arrives in a query string or request body for a site-bound caller — read the gateway-injected X-Site-Id (or SiteContext) instead.