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) orGLOBAL(cross-site / platform view).
| Principal | JWT siteId | Effective X-Site-Id |
|---|---|---|
Site-bound user / SITE_ADMIN | S | forced 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) |
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();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.