Multi-tenant SaaS applications are revolutionizing how businesses deliver software, allowing multiple customers (tenants) to share a single platform while keeping their data and sessions isolated. Whether you're building an admin portal or an employee portal, secure authentication is critical to ensure each tenant's experience is private and seamless.
In this Blog, I'll walk through creating a robust multi-tenant authentication system using "js-cookie "
for secure session management and React for a dynamic, tenant-aware frontend. Leveraging a monorepo setup with tools like eslint-config-custom
and Turbo scripts ( turbo run lint
), we'll build a scalable solution for SaaS platforms.
Why js-cookie and React?
js-cookie: Lightweight library for managing cookies securely with options like secure
, sameSite
, and subdomain sharing.
React: Ideal for dynamic UIs, using hooks and context to manage tenant-based authentication state.
Custom Auth Module: A reusable package in your monorepo for tenant-specific logic.
2025 Security Trends: Emphasize strict cookie policies, subdomain sharing, and server-side validation for multi-tenant SaaS apps.
This combination delivers fast, secure, and scalable authentication for multi-tenant environments.
Prerequisites
Ensure your package.json
includes:
"js-cookie": "^3.0.5"
and "@types/js-cookie": "^3.0.6"
"eslint": "^8.22.0"
and "eslint-config-custom": "workspace:*"
A monorepo with packages like admin-portal
, operator-portal
, and auth
.
Install dependencies
npm install js-cookie @types/js-cookie eslint react-router-dom
Project structure
project/
βββ packages/
β βββ admin-portal/
β βββ auth/
β βββ component-library/
βββ src/
β βββ auth.js
Step 1. Tenant-Based Authentication Module
Create packages/auth/src/index.js
to handle tenant-specific sessions:
import Cookies from 'js-cookie';
// Auth service for tenant-based sessions
export const AuthService = {
login: async (tenantId, userCredentials) => {
try {
const response = await fetch(`/api/auth/${tenantId}/login`, {
method: 'POST',
body: JSON.stringify(userCredentials),
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) throw new Error('Login failed');
const { token } = await response.json();
Cookies.set(`auth_token_${tenantId}`, token, {
expires: 1,
secure: true,
sameSite: 'Strict',
path: '/',
});
return token;
} catch (error) {
throw new Error(`Login error: ${error.message}`);
}
},
getToken: (tenantId) => Cookies.get(`auth_token_${tenantId}`),
logout: (tenantId) => Cookies.remove(`auth_token_${tenantId}`, { path: '/', secure: true }),
isAuthenticated: (tenantId) => !!Cookies.get(`auth_token_${tenantId}`),
};
Security Highlights
Tenant Isolation with unique cookie keys.
Secure cookies ( secure
, sameSite
).
Optional subdomain sharing via domain
.
Clear error handling.
Step 2. Integrating with React
Use React Context in packages/admin-portal/src/App.jsx
:
import React, { createContext, useState, useEffect, useContext } from 'react';
import { AuthService } from '../auth';
import { useNavigate } from 'react-router-dom';
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [tenantId, setTenantId] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const navigate = useNavigate();
useEffect(() => {
const hostname = window.location.hostname;
const tenant = hostname.includes('.') ? hostname.split('.')[0] : 'default';
setTenantId(tenant);
setIsAuthenticated(AuthService.isAuthenticated(tenant));
}, []);
const login = async (credentials) => {
try {
await AuthService.login(tenantId, credentials);
setIsAuthenticated(true);
navigate('/dashboard');
} catch (error) {
alert('Login failed');
}
};
const logout = () => {
AuthService.logout(tenantId);
setIsAuthenticated(false);
navigate('/login');
};
return (
<AuthContext.Provider value={{ tenantId, isAuthenticated, login, logout }}>
{children}
</AuthContext.Provider>
);
};
const LoginPage = () => {
const { login, tenantId } = useContext(AuthContext);
const handleSubmit = (e) => {
e.preventDefault();
const credentials = { username: e.target.username.value, password: e.target.password.value };
login(credentials);
};
return (
<div style={{ maxWidth: '400px', margin: '50px auto' }}>
<h2>Login for Tenant: {tenantId}</h2>
<form onSubmit={handleSubmit}>
<input type="text" name="username" placeholder="Username" />
<input type="password" name="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
</div>
);
};
Step 3. Securing with ESLint
Add security-focused linting:
module.exports = {
rules: {
'no-eval': 'error',
'no-console': ['warn', { allow: ['error'] }],
'security/detect-object-injection': 'error',
'no-unused-vars': ['error', { vars: 'all', args: 'none' }],
},
plugins: ['security'],
env: { browser: true, node: true },
};
turbo run lint
Step 4. Optimization and Security Best Practices
Use HttpOnly
cookies and server-side JWT validation.
Validate tenantId
on the backend.
Cache tenant configs for performance.
Share AuthService
across portals in the monorepo.
Enable subdomain cookie sharing when needed.
Queue login events with @azure/service-bus
.
Use luxon
for timezone-aware logging.
Real-World Use Case
Imagine a SaaS platform where companies manage their teams via isolated dashboards. A tenant logs in at tenant1.yourapp.com
, and their session is stored securely with js-cookie
. The system scales to thousands of tenants worldwide, handling spikes and logging accurately across regions.
Conclusion
With js-cookie
and React, you can craft a secure, multi-tenant authentication system thatβs both developer-friendly and production-ready. By combining a custom auth module, enforcing security with ESLint, and optimizing with Turboβs workflows, youβll ensure scalability and safety for SaaS apps.