UI per Tenant
- Anand Nerurkar
- Apr 26
- 7 min read
Updated: Apr 29
how do we diesign UI for multi tenat saas architecture
Great question! Designing the UI for a multi-tenant SaaS architecture requires both technical and UX considerations to support multiple tenants (clients/organizations), each potentially with their own branding, configurations, roles, and permissions—while sharing a single codebase.
Here's a structured breakdown of how to approach UI design for a multi-tenant SaaS system:
🔷 1. Tenant-Aware UI Design Principles
✅ Shared UI with Tenant Context
Use a single UI codebase that dynamically adapts to the tenant.
Pass tenant ID or slug (e.g., app.com/tenant1) to the frontend at login time or embed in token.
Fetch tenant-specific settings from a Tenant Config Service.
🔷 2. Tenant-Specific Branding & Theming
💡 Strategy:
Allow each tenant to configure:
Logo, brand colors, fonts
Welcome text, favicon
Custom domain (e.g., portal.tenantx.com)
🛠️ How:
Use a theming engine (e.g., Tailwind + CSS variables or Material-UI theme overrides).
Fetch brand settings via API on login or initial load and cache locally.
Apply branding dynamically using context-aware ThemeProvider.
js
<ThemeProvider theme={tenantTheme}> <App /> </ThemeProvider>
🔷 3. Role-Based Access Control (RBAC)
👥 Per-tenant RBAC
UI must dynamically adjust menus, routes, and actions based on:
Tenant role
User permissions (read-only, admin, approver)
🛠️ How:
Centralized route config with metadata for access control.
Show/hide components or menu items based on user roles.
Use React Context or Redux for user + role data injection.
🔷 4. Feature Flag Support per Tenant
Enable/disable features per tenant using a feature flag service (e.g., LaunchDarkly, Unleash).
This allows controlled rollout, A/B testing, or premium features.
ts
if (flags['show-new-dashboard']) { return <NewDashboard />; } else { return <LegacyDashboard />; }
🔷 5. Tenant-aware Routing & URLs
Use subdomain-based or path-based routing:
Approach | Example |
Subdomain-based | |
Path-based |
Ensure all API calls and internal links pass the tenant ID appropriately.
🔷 6. Tenant-Specific Localization & Content
Allow tenants to define custom terms (e.g., “Employee” → “Crew Member”).
Store in i18n files or tenant-specific content DB.
Load language packs or content overrides at runtime.
🔷 7. Onboarding and Config Wizards
Provide UI wizards for:
Admin onboarding (company info, users, roles)
Tenant-specific configurations
Billing or subscription setup
🔷 8. Error Handling & SLA Awareness
Display tenant-specific support links or escalation options.
Track SLA breaches and errors using telemetry and show user-friendly fallback UIs.
🔷 9. Scalable UI Architecture
Follow component-based design, e.g., React with Storybook for reusable components.
Each component should accept tenantSettings or theme as a prop/context.
Consider Micro Frontends if large teams work on isolated modules.
🔷 Example Folder Structure (React)
bash
/src /themes tenant-default.js tenant-acme.js /contexts TenantContext.js AuthContext.js /components NavBar.js Sidebar.js Dashboard.js /services tenant-config.service.js auth.service.js /routes tenantRoutes.js
💡 Pro Tip:
Build a Tenant Switcher Admin Panel if you need to impersonate or troubleshoot tenants quickly (used in enterprise SaaS support).
how does macrofrot end support multi tenant UI architecture
When designing micro frontend (MFE) architecture for multi-tenant SaaS UI, you’re combining the power of independent feature teams with tenant-aware rendering, theming, and routing.
Here’s how micro frontends (MFEs) support multi-tenant UI in an enterprise SaaS setup:
🧱 1. Each MFE Is Tenant-Aware
Every micro frontend (e.g., Billing, Dashboard, Admin Console) is built to:
Accept tenant context as props or via a shared context/provider
Dynamically apply tenant-specific themes, features, or UI elements
This ensures each MFE can:
Render different logos, colors, or components
Respect per-tenant permissions (RBAC)
Enable/disable modules or views (via feature flags)
🧩 2. Composition Layer (Shell App / Host App)
A Shell App (a.k.a. "layout" or "host" app) handles:
Parsing the tenant ID (from subdomain, JWT, or URL path)
Fetching tenant config (e.g., branding, features)
Setting up global context (TenantContext, AuthContext, ThemeProvider)
Loading MFEs via Module Federation or other strategies (like Webpack 5, Single-SPA)
bash
CopyEdit
https://<tenant-id>.yourapp.com/dashboard → Shell → passes tenant info to MFEs
🖼️ 3. Shared Context and Theming
Tenant-aware theming and branding are achieved through:
Global ThemeProvider (e.g., styled-components, MUI Theme)
Shared TenantContext, made available to all MFEs
Shared libraries for layout, authentication, and config utilities
MFEs consume shared context and adapt:
tsx
CopyEdit
const { theme, features } = useContext(TenantContext); return <Header style={{ backgroundColor: theme.primary }} />;
🚥 4. Routing & Access Control per Tenant
Tenant-based routing options:
Subdomain-based: tenant1.yourapp.com
Path-based: yourapp.com/tenant1/dashboard
Inside each MFE or the shell:
Conditionally render routes or views based on tenant permissions and feature flags
Use centralized RBAC config (role, tenant, module, action)
🛠️ 5. Deployment Strategies
Approach | Description |
Module Federation | Each MFE independently deployable, loaded dynamically by Shell via remote entry |
iFrame (legacy) | Each MFE runs in isolation (less preferred due to performance & UX gaps) |
Single-SPA | Compose MFEs from different frameworks with lifecycle management |
🔐 6. Multi-Tenant Security
Token includes tenant ID, scoped permissions
MFEs validate the token and tenant context
Prevent cross-tenant access by validating tenant-bound API access
✅ Example: MFE Stack for SaaS
Component | Tech Stack Example |
Shell App | React + Webpack 5 + Module Federation |
Auth MFE | React + Keycloak/OIDC |
Billing MFE | Angular + Stripe SDK |
Reports MFE | Vue + D3 |
Shared Libraries | TypeScript + Tailwind/MUI + i18n |
API Gateway | Multi-tenant JWT, Tenant-aware routing |
💡 Summary – Key Benefits of MFE for Multi-Tenant SaaS
✅ Decoupled team ownership✅ Per-tenant customization✅ Faster release cycle✅ Scalable theming and RBAC✅ Isolated deployment of features
how do you design API for multi tenant architecture
Designing APIs for a multi-tenant SaaS architecture involves careful consideration of security, data isolation, scalability, and tenant context propagation. Here's a structured and enterprise-ready guide to help you design robust, scalable multi-tenant APIs.
🔧 1. Tenant Context Passing
📌 Common Patterns
Subdomain: tenant1.myapp.com/api/v1/...
Path: myapp.com/api/v1/tenant1/...
Header: X-Tenant-ID: tenant1
JWT Claims: Include tenant_id in the token payload
Best practice: Use JWT-based authentication with tenant_id in claims to ensure secure and validated tenant identity on every request.
🛡️ 2. Tenant Isolation & Security
Every API must validate the tenant_id against the authenticated user’s token
Never trust input directly from URL/path/header — validate it server-side
Enforce row-level security in DB using tenant ID
Apply role-based access control (RBAC) within the tenant scope
java
CopyEdit
// Sample Java Spring Security @PreAuthorize("hasRole('ADMIN') and #tenantId == authentication.token.tenantId") public ResponseEntity<?> getTenantUsers(@PathVariable String tenantId) { ... }
🧩 3. API Design Patterns
Concern | Pattern | Example |
URL Structure | Scoped Path or Header | /api/v1/tenantA/users |
Auth | JWT with tenant ID claim | {"sub": "user1", "tenant": "tenantA"} |
RBAC | Tenant-scoped roles | tenant_admin, tenant_user |
DB Access | Row-level isolation | WHERE tenant_id = ? |
Service Layer | Tenant Context Injection | @Inject TenantContext |
API Gateway | Multi-tenant routing / auth | Validate tenant before request hits microservice |
🗄️ 4. Data Partitioning Strategies
🏢 1. Shared DB, Shared Schema
All tenants in same DB and same tables
Use tenant_id column for partitioning
✅ Cost-effective, good for early stage
⚠️ Must enforce row-level access control
🏢 2. Shared DB, Separate Schema
Each tenant has its own schema (tenantA.users)
Better isolation, more complexity
🏢 3. Separate DB per tenant
Full isolation
Costlier, ideal for large/regulated customers
Useful for data residency laws (e.g., GDPR)
🔁 5. Multi-Tenant Middleware
Use a middleware/interceptor to:
Extract and validate tenant_id
Load tenant config, theme, limits
Inject TenantContext into request/thread
java
public class TenantContextInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String tenantId = extractTenantId(request); validate(tenantId); TenantContext.set(tenantId); return true; } }
📈 6. Monitoring, Throttling, and Limits
Track usage per tenant (API metrics, limits, abuse detection)
Use API Gateway (e.g., Kong, Apigee, Azure API Management) to:
Enforce rate limits per tenant
Log tenant-specific metrics
🧪 7. Testing and Observability
Write multi-tenant test cases (simulate multiple tenants with different data sets)
Include tenant ID in all logs and metrics
Ensure data visibility is scoped properly in logs, APM, alerts
🧠 Best Practices
Practice | Why it Matters |
Validate tenant identity | Prevent data leaks or tenant spoofing |
Isolate data access | Enforce via app logic + DB level if needed |
Use consistent tenant key | In APIs, services, logs, metrics |
Make tenant context injectable | For logging, auditing, personalization |
Plan for scalability | Especially if using shared DB/infra |
🔚 Summary
A multi-tenant API should: ✅ Accept and validate tenant context✅ Enforce strict tenant-bound data access✅ Propagate tenant info across services✅ Be observable and rate-limited per tenant✅ Scale flexibly as tenants grow
Great! Let's walk through a live example of a multi-tenant Spring Boot app with:
Tenant-aware API
Multi-DB isolation
OAuth2 security
Rate limiting
UI routing via subdomain
🎯 Use Case
Scenario: A SaaS HR platform with different tenants like acme.hrplatform.com, globex.hrplatform.com.
Each tenant should:
Have isolated DB
Be authenticated via OAuth2
Be rate limited
Get custom UI theme and routes
1️⃣ Tenant Identification (Subdomain-Based)
Create a filter:
java
CopyEdit
@Component public class TenantFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException { String host = req.getServerName(); // acme.hrplatform.com String tenantId = host.split("\\.")[0]; // acme TenantContext.setCurrentTenant(tenantId); chain.doFilter(req, res); } }
2️⃣ Multi-DB Routing
Use AbstractRoutingDataSource:
java
CopyEdit
public class MultiTenantDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return TenantContext.getCurrentTenant(); // e.g., acme } }
In your config:
java
CopyEdit
@Bean public DataSource dataSource() { Map<Object, Object> resolvedDataSources = new HashMap<>(); tenants.forEach((id, props) -> { resolvedDataSources.put(id, DataSourceBuilder.create().url(props.getUrl()).build()); }); MultiTenantDataSource ds = new MultiTenantDataSource(); ds.setTargetDataSources(resolvedDataSources); return ds; }
3️⃣ Tenant-Aware API
java
CopyEdit
@RestController @RequestMapping("/employees") public class EmployeeController { @Autowired private EmployeeService service; @GetMapping public List<Employee> getAll() { String tenantId = TenantContext.getCurrentTenant(); return service.getAllForTenant(tenantId); } }
4️⃣ OAuth2 with Spring Security
In application.yml:
yaml
CopyEdit
spring: security: oauth2: resourceserver: jwt: issuer-uri: https://auth.hrplatform.com
In your security config:
java
CopyEdit
@EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests() .anyRequest().authenticated() .and() .oauth2ResourceServer().jwt(); return http.build(); } }
5️⃣ Rate Limiting (Per Tenant with Bucket4j)
java
CopyEdit
@Component public class RateLimitInterceptor implements HandlerInterceptor { private final Map<String, Bucket> cache = new ConcurrentHashMap<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String tenantId = TenantContext.getCurrentTenant(); Bucket bucket = cache.computeIfAbsent(tenantId, k -> Bucket4j.builder() .addLimit(Bandwidth.simple(100, Duration.ofMinutes(1))).build()); if (!bucket.tryConsume(1)) { response.setStatus(429); return false; } return true; } }
6️⃣ Frontend Routing (React Example)
js
CopyEdit
const hostname = window.location.hostname; const tenantId = hostname.split('.')[0]; // 'acme' const theme = getThemeForTenant(tenantId); return ( <ThemeProvider theme={theme}> <Routes> <Route path="/" element={<Dashboard tenant={tenantId} />} /> </Routes> </ThemeProvider> );
Would you like me to spin this up as a GitHub repo, create a Docker Compose, or simulate a request lifecycle (with headers and trace)?
Comments