How to design a reliable session and state synchronization layer in hybrid client-server apps to prevent authentication drops during tenant switching.
In multi-tenant business platforms (like ERP or billing systems), users frequently manage or switch between multiple companies. Designing this flow requires a robust session management system.
A common issue in hybrid SPA architectures is the loss of session persistence: users refreshing the browser or switching between companies suddenly find themselves logged out due to state mismatches between the client local storage and the server session cookies. Let's look at how to build a synchronization layer to resolve this.
When a user switches from "Company A" to "Company B," the application makes API requests to fetch new datasets. If the client updates its internal active_company_id in LocalStorage, but the server's session token or CSRF cookie is not correctly renewed or linked, request interceptors will return a 401 Unauthorized or 419 Page Expired error, forcing an unnecessary logout.
To prevent token drops during quick navigation, use an Axios interceptor that dynamically syncs active headers on every outgoing request.
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// lib/http-client.ts
import axios from 'axios';
const httpClient = axios.create({
baseURL: '/api',
withCredentials: true, class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Crucial to pass server-side cookies
});
httpClient.interceptors.request.use((config) => {
const activeCompanyId = localStorage.getItem('active_company_id');
if (activeCompanyId) {
config.headers['X-Company-ID'] = activeCompanyId;
}
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Automatically retrieve the CRSF token from document cookies for Laravel/Sanctum
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('XSRF-TOKEN='))
?.split('=')[1];
if (csrfToken) {
config.headers['X-XSRF-TOKEN'] = decodeURIComponent(csrfToken);
}
return config;
}, (error) => {
return Promise.reject(error);
});In your backend framework, verify that the requested company ID header matches the list of companies associated with the authenticated user. If a mismatch or session expiry is detected, handle the response gracefully rather than wiping the session.
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// app/Http/Middleware/TenantContextMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class TenantContextMiddleware
{
public function handle(Request $request, Closure $next)
{
$companyId = $request->header('X-Company-ID');
$user = $request->user();
if ($companyId && $user) {
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Confirm the authenticated user belongs to the requested company
$hasAccess = $user->companies()->where('companies.id', $companyId)->exists();
if ($hasAccess) {
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Bind the active tenant globally to the request context
app()->instance('active_company', $user->companies()->find($companyId));
} else {
return response()->json([
'error' => 'Unauthorized company context. Please refresh your session.'
], 403);
}
}
return $next($request);
}
}If an HTTP request fails with a 401 or 419 error code, try to silently refresh the session token before forcing the user back to the login screen.
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// lib/http-client.ts (Response Interceptor)
httpClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if ((error.response?.status === 401 || error.response?.status === 419) && !originalRequest._retry) {
originalRequest._retry = true;
try {
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Request a silent CSRF/Session token renewal
await axios.get('/api/csrf-cookie');
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Retry the original request
return httpClient(originalRequest);
} catch (refreshError) {
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Force redirection to login only if silent renewal fails
localStorage.removeItem('active_company_id');
window.location.href = '/login?session_expired=true';
}
}
return Promise.reject(error);
}
);Using a synchronized interceptor and context validation middleware preserves user sessions during navigation and simplifies multi-company account handling.