@mapnests/gateway-web-sdk
v1.0.9
Published
Session token management SDK with automatic refresh for React/Next.js applications
Maintainers
Readme
Gateway Session SDK
A lightweight, production-ready session token management SDK for React and Next.js applications. Handles automatic token refresh with configurable intervals to prevent race conditions and token expiration issues.
Features
- Automatic token refresh with configurable background intervals
- HttpOnly cookie support for secure token storage
- React hook (
useSession) for seamless integration - Singleton pattern ensuring a single session instance across your app
- Zero runtime dependencies (
reactandreact-domas peer dependencies) - Next.js compatible (SSR-safe)
- Built-in Fetch and Axios interceptors with automatic session readiness gating and 401 handling
- Resilient to browser close/reopen — stale cookies are cleared and tokens refresh before any API call
- Full TypeScript definitions included
Installation
npm install @mapnests/gateway-web-sdkImplementation Guide
Choose one of the three approaches below based on your preferred HTTP client. All three approaches share the same Step 1 (environment setup) and Step 2 (session initialization).
Next.js users: Skip the Common Setup below and go directly to the Next.js Integration section, which provides its own Steps 1–2. Then return here for Approach A, B, or C.
Common Setup (Vite / CRA / React Apps)
Step 1 — Environment Variables
Create a .env file in your project root with the following variables:
VITE_API_BASE_URL=https://your-gateway.example.com
VITE_BOOTSTRAP_PATH=/api/session/bootstrap
VITE_TOKEN_COOKIE_NAME=token| Variable | Description |
|----------|-------------|
| VITE_API_BASE_URL | Base URL of your API gateway |
| VITE_BOOTSTRAP_PATH | Path to the session bootstrap endpoint |
| VITE_TOKEN_COOKIE_NAME | Name of the token cookie set by your server |
Then create a config helper to read these values:
// src/config.js
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
const BOOTSTRAP_PATH = import.meta.env.VITE_BOOTSTRAP_PATH;
const TOKEN_COOKIE_NAME = import.meta.env.VITE_TOKEN_COOKIE_NAME;
if (!API_BASE_URL) throw new Error('VITE_API_BASE_URL is not defined');
if (!BOOTSTRAP_PATH) throw new Error('VITE_BOOTSTRAP_PATH is not defined');
if (!TOKEN_COOKIE_NAME) throw new Error('VITE_TOKEN_COOKIE_NAME is not defined');
export { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME };Step 2 — Initialize the Session Manager
Configure and initialize the SDK once at your app's entry point:
// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { SessionManager } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME } from './config.js';
import App from './App';
const sessionManager = SessionManager.getInstance();
sessionManager.configure({
bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
tokenCookieName: TOKEN_COOKIE_NAME,
});
sessionManager.initialize().catch(err =>
console.error('Failed to initialize session:', err)
);
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);Now proceed with one of the three approaches below.
Approach A — Fetch Interceptor
The simplest option. Drop-in replacement for fetch that automatically handles session headers and 401 retry.
Step 3 — Create the API layer
// src/api.js
import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL } from './config.js';
export const getUser = () =>
fetchInterceptor(`${API_BASE_URL}/api/user`);Step 4 — Use in a component
// src/Dashboard.jsx
import { useEffect, useState } from 'react';
import { useSession } from '@mapnests/gateway-web-sdk';
import { getUser } from './api.js';
export default function Dashboard() {
const { isInitialized, isLoading, error } = useSession();
const [user, setUser] = useState(null);
useEffect(() => {
if (!isInitialized) return;
getUser()
.then(res => res.json())
.then(setUser)
.catch(err => console.error('Failed to fetch user:', err));
}, [isInitialized]);
if (isLoading) return <p>Loading session...</p>;
if (error) return <p>Session error: {error}</p>;
if (!user) return <p>Loading data...</p>;
return <pre>{JSON.stringify(user, null, 2)}</pre>;
}Approach B — Axios Interceptor
Best if you already use Axios. Wraps an Axios instance with automatic session headers and 401 retry.
Requires
axiosas a dependency:npm install axios
Step 3 — Create the Axios instance and API layer
// src/api.js
import axios from 'axios';
import { setupAxiosInterceptor } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL } from './config.js';
const api = setupAxiosInterceptor(
axios.create({
baseURL: API_BASE_URL,
withCredentials: true,
})
);
export const getUser = () => api.get('/api/user');Step 4 — Use in a component
// src/Dashboard.jsx
import { useEffect, useState } from 'react';
import { useSession } from '@mapnests/gateway-web-sdk';
import { getUser } from './api.js';
export default function Dashboard() {
const { isInitialized, isLoading, error } = useSession();
const [user, setUser] = useState(null);
useEffect(() => {
if (!isInitialized) return;
getUser()
.then(res => setUser(res.data))
.catch(err => console.error('Failed to fetch user:', err));
}, [isInitialized]);
if (isLoading) return <p>Loading session...</p>;
if (error) return <p>Session error: {error}</p>;
if (!user) return <p>Loading data...</p>;
return <pre>{JSON.stringify(user, null, 2)}</pre>;
}Approach C — Manual Implementation
Full control over request construction and 401 handling. Use this when you need custom logic or don't want to use the built-in interceptors.
Step 3 — Create a manual fetch wrapper
// src/api.js
import { SessionManager } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL } from './config.js';
const sm = SessionManager.getInstance();
async function request(url, init = {}) {
// Ensure session is bootstrapped before sending (handles browser close/reopen, tab wake, etc.)
await sm.ensureReady();
const opts = {
...init,
credentials: 'include',
headers: { ...(init.headers || {}) },
};
// Attach session headers
opts.headers['cf-session-id'] = sm.getSessionId();
opts.headers['x-client-platform'] = 'web';
if (sm.shouldUseTokenHeader()) {
const token = sm.getToken(sm.config.tokenCookieName);
if (token) opts.headers[sm.config.tokenCookieName] = token;
}
let res = await fetch(url, opts);
// Handle 401 with the configured invalidSessionError
if (res.status === 401) {
const cloned = res.clone();
try {
const body = await cloned.json();
if (body.error_msg === sm.config.invalidSessionError) {
// Wait for any in-progress refresh, or trigger a new one
if (sm.isRefreshing()) {
await sm.waitForRefresh();
} else {
await sm.refreshToken();
}
// Update headers with refreshed session
opts.headers['cf-session-id'] = sm.getSessionId();
if (sm.shouldUseTokenHeader()) {
const newToken = sm.getToken(sm.config.tokenCookieName);
if (newToken) opts.headers[sm.config.tokenCookieName] = newToken;
}
// Retry the request
res = await fetch(url, opts);
}
} catch {
// Response was not JSON — return the original response
}
}
return res;
}
export const getUser = () => request(`${API_BASE_URL}/api/user`);Step 4 — Use in a component
// src/Dashboard.jsx
import { useEffect, useState } from 'react';
import { useSession } from '@mapnests/gateway-web-sdk';
import { getUser } from './api.js';
export default function Dashboard() {
const { isInitialized, isLoading, error } = useSession();
const [user, setUser] = useState(null);
useEffect(() => {
if (!isInitialized) return;
getUser()
.then(res => res.json())
.then(setUser)
.catch(err => console.error('Failed to fetch user:', err));
}, [isInitialized]);
if (isLoading) return <p>Loading session...</p>;
if (error) return <p>Session error: {error}</p>;
if (!user) return <p>Loading data...</p>;
return <pre>{JSON.stringify(user, null, 2)}</pre>;
}Next.js Integration
Note: For Next.js, use
NEXT_PUBLIC_prefixed environment variables instead ofVITE_, and replace the common Step 1 config helper and Step 2 initialization with the Next.js-specific setup below.
Step 1 — Environment Variables
NEXT_PUBLIC_API_BASE_URL=https://your-gateway.example.com
NEXT_PUBLIC_BOOTSTRAP_PATH=/api/session/bootstrap
NEXT_PUBLIC_TOKEN_COOKIE_NAME=tokenStep 2 — Config Helper
// src/config.js
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
const BOOTSTRAP_PATH = process.env.NEXT_PUBLIC_BOOTSTRAP_PATH;
const TOKEN_COOKIE_NAME = process.env.NEXT_PUBLIC_TOKEN_COOKIE_NAME;
if (!API_BASE_URL) throw new Error('NEXT_PUBLIC_API_BASE_URL is not defined');
if (!BOOTSTRAP_PATH) throw new Error('NEXT_PUBLIC_BOOTSTRAP_PATH is not defined');
if (!TOKEN_COOKIE_NAME) throw new Error('NEXT_PUBLIC_TOKEN_COOKIE_NAME is not defined');
export { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME };App Router
Step 3 — Create a Session Provider
Create a client component that initializes the session. This keeps the root layout as a Server Component, preserving the benefits of React Server Components.
// app/providers/SessionProvider.jsx
'use client';
import { useEffect } from 'react';
import { SessionManager } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME } from '@/src/config';
// Configure at module level so the SDK is ready before any child component mounts.
// configure() only sets config values — no browser APIs needed, safe during SSR.
const sessionManager = SessionManager.getInstance();
sessionManager.configure({
bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
tokenCookieName: TOKEN_COOKIE_NAME,
});
export default function SessionProvider({ children }) {
// initialize() calls the bootstrap API — must run in useEffect (client-only)
useEffect(() => {
sessionManager.initialize().catch(err =>
console.error('Failed to initialize session:', err)
);
}, []);
return children;
}Step 4 — Add the Provider to the Root Layout
The root layout stays as a Server Component — do not add 'use client' here.
// app/layout.js
import SessionProvider from './providers/SessionProvider';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<SessionProvider>{children}</SessionProvider>
</body>
</html>
);
}Step 5 — Create an API Layer and Use in a Page
After your router setup is complete, create an API layer using any of Approach A / B / C from the Implementation Guide above. The only change needed is to replace import.meta.env.VITE_* references with imports from your src/config.js.
For example, using Approach A (Fetch Interceptor):
// src/api.js
import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL } from './config';
export const getUser = () =>
fetchInterceptor(`${API_BASE_URL}/api/user`);Then use it in a page component. Page components that use hooks must be client components.
// app/dashboard/page.jsx
'use client';
import { useEffect, useState } from 'react';
import { useSession } from '@mapnests/gateway-web-sdk';
import { getUser } from '@/src/api';
export default function DashboardPage() {
const { isInitialized, isLoading, error } = useSession();
const [user, setUser] = useState(null);
useEffect(() => {
if (!isInitialized) return;
getUser()
.then(res => res.json())
.then(setUser)
.catch(err => console.error('Failed to fetch user:', err));
}, [isInitialized]);
if (isLoading) return <p>Loading session...</p>;
if (error) return <p>Session error: {error}</p>;
if (!user) return <p>Loading data...</p>;
return <pre>{JSON.stringify(user, null, 2)}</pre>;
}Pages Router
Step 3 — Initialize in _app.js
// pages/_app.js
import { useEffect } from 'react';
import { SessionManager } from '@mapnests/gateway-web-sdk';
import { API_BASE_URL, BOOTSTRAP_PATH, TOKEN_COOKIE_NAME } from '../src/config';
// Configure at module level — runs once when the module is first imported.
const sessionManager = SessionManager.getInstance();
sessionManager.configure({
bootstrapUrl: `${API_BASE_URL}${BOOTSTRAP_PATH}`,
tokenCookieName: TOKEN_COOKIE_NAME,
});
function MyApp({ Component, pageProps }) {
// initialize() needs the browser — must run in useEffect
useEffect(() => {
sessionManager.initialize().catch(err =>
console.error('Failed to initialize session:', err)
);
}, []);
return <Component {...pageProps} />;
}
export default MyApp;Step 4 — Create an API Layer and Use in a Page
Same as the App Router — create an src/api.js using any of Approach A / B / C, then use it in your page:
// pages/dashboard.jsx
import { useEffect, useState } from 'react';
import { useSession } from '@mapnests/gateway-web-sdk';
import { getUser } from '../src/api';
export default function Dashboard() {
const { isInitialized, isLoading, error } = useSession();
const [user, setUser] = useState(null);
useEffect(() => {
if (!isInitialized) return;
getUser()
.then(res => res.json())
.then(setUser)
.catch(err => console.error('Failed to fetch user:', err));
}, [isInitialized]);
if (isLoading) return <p>Loading session...</p>;
if (error) return <p>Session error: {error}</p>;
if (!user) return <p>Loading data...</p>;
return <pre>{JSON.stringify(user, null, 2)}</pre>;
}API Reference
useSession(options?)
React hook for session management.
Parameters:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| options.autoInitialize | boolean | true | Automatically initialize session on mount |
Returns:
{
isInitialized: boolean;
isLoading: boolean;
error: string | null;
lastRefreshTime: number | null;
nextRefreshTime: number | null;
timeUntilRefresh: number | null;
initializationFailed: boolean;
refresh: () => Promise<void>;
initialize: () => Promise<void>;
}SessionManager
Core session management class (Singleton).
SessionManager.getInstance()
Returns the singleton instance.
configure(config)
sessionManager.configure({
bootstrapUrl: '/session/bootstrap', // Required: Bootstrap API endpoint
tokenCookieName: 'token', // Optional: Token cookie name (default: 'token')
maxRetries: 3, // Optional: Max retry attempts (default: 3)
headers: {}, // Optional: Additional headers for bootstrap calls
credentials: true, // Optional: Include credentials (default: true)
logLevel: 'WARN', // Optional: 'NONE' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG'
});
refreshIntervalandtokenExpiryare automatically set by the server's bootstrap response (refresh_timeandexpire_timefields). You don't need to configure them manually.
initialize()
Initialize session by calling the bootstrap endpoint.
await sessionManager.initialize();refreshToken()
Manually trigger a session token refresh.
await sessionManager.refreshToken();getSessionId()
Returns the current cf-session-id.
getToken(name?)
Returns the token value from the named cookie, or null.
shouldUseTokenHeader()
Returns true if the token should be sent as a request header (i.e. when the app is served over HTTP, not HTTPS).
ensureReady()
Ensures the session is ready before making API calls. This is the recommended way to gate requests on session readiness.
- If initialized and token is fresh, resolves immediately (zero overhead).
- If initialization is in progress, waits for it to complete.
- If not initialized but configured, triggers
initialize()and waits. - If not configured or permanently failed, resolves and lets the request proceed (the 401 handler will deal with it).
await sessionManager.ensureReady();
// Session is now ready — safe to make API callsNote:
fetchInterceptorandsetupAxiosInterceptorcallensureReady()automatically before every request. You only need to call it explicitly when building a manual fetch wrapper (Approach C).
needsRefresh()
Returns true if the session is initialized but the token has likely expired. Useful for proactive checks.
if (sessionManager.needsRefresh()) {
await sessionManager.refreshToken();
}isRefreshing()
Returns true if a refresh/initialize is currently in progress.
waitForRefresh()
Returns a promise that resolves when the in-progress refresh completes.
getSessionStatus()
Returns the current session state object.
{
isInitialized: boolean;
isLoading: boolean;
lastRefreshTime: number | null;
nextRefreshTime: number | null;
tokenExpiry: number | null;
error: string | null;
errorCode: string | null;
initializationFailed: boolean;
timeUntilRefresh: number | null;
}Note:
getSessionStatus()returnstokenExpiryanderrorCodewhich are not exposed by theuseSessionhook. Use this method directly if you need those fields.
subscribe(listener)
Subscribe to session state changes. Returns an unsubscribe function.
const unsubscribe = sessionManager.subscribe((state) => {
console.log('Session state:', state);
});destroy()
Clean up timers, listeners, and cookies. Resets the session manager.
fetchInterceptor(url, options?)
Drop-in fetch wrapper. Automatically gates on session readiness via ensureReady(), attaches session headers, and retries on 401 INVALID_SESSION. Safe to call even before the session is initialized — the request will wait for bootstrap to complete.
import { fetchInterceptor } from '@mapnests/gateway-web-sdk';
const response = await fetchInterceptor('/api/data');setupAxiosInterceptor(axiosInstance)
Attaches request/response interceptors to an Axios instance. Returns the same instance. Every request automatically gates on session readiness via ensureReady(), attaches session headers, and retries on 401 INVALID_SESSION.
import axios from 'axios';
import { setupAxiosInterceptor } from '@mapnests/gateway-web-sdk';
const api = setupAxiosInterceptor(axios.create({ baseURL: '/api' }));Error Classes
The SDK exports custom error classes for typed error handling:
import {
SessionError, // Base error class (code, details)
ConfigurationError, // Invalid configuration (code: 'CONFIGURATION_ERROR')
BootstrapError, // Bootstrap API failure (code: 'BOOTSTRAP_ERROR')
NetworkError, // Network-level failure (code: 'NETWORK_ERROR')
SSRError, // Called in non-browser environment (code: 'SSR_ERROR')
} from '@mapnests/gateway-web-sdk';All errors extend SessionError, which provides code (string) and details (object) properties.
logger and LOG_LEVELS
The SDK's internal logger is exported for advanced use (e.g. setting log level independently of configure()):
import { logger, LOG_LEVELS } from '@mapnests/gateway-web-sdk';
logger.setLevel('DEBUG'); // or logger.setLevel(LOG_LEVELS.DEBUG)
logger.info('Custom log'); // [SessionManager] Custom logAvailable levels: NONE (0), ERROR (1), WARN (2, default), INFO (3), DEBUG (4).
Security Notice
This SDK prioritizes server-set HttpOnly cookies for maximum security. The SDK includes a fallback to set cookies client-side, but these cannot be HttpOnly and are accessible to JavaScript.
Recommended: Always set cookies from your server using the Set-Cookie header with HttpOnly flag. The SDK will detect server cookies and skip client-side cookie setting automatically.
Best Practices
- Single Instance — Call
configure()once at app startup and reuse the singleton. - Token Timing — The server controls refresh and expiry timing via the bootstrap response.
- Error Handling — Handle errors gracefully and provide user feedback for limited mode.
- HTTPS — Always use HTTPS in production environments.
- CORS/Credentials — If cross-origin, ensure your server CORS allows credentials.
- Initialization Order — Always call
configure()theninitialize()before making API calls.
Troubleshooting
Cookies not being set
- Verify your API returns
Set-Cookieheader withHttpOnlyflag - Check CORS configuration allows credentials
- Ensure
credentials: truein configuration
Automatic refresh not working
- Check browser console for errors
- Verify server is sending
refresh_timein bootstrap response - Ensure timer isn't being cleared prematurely
Multiple initializations
- The SDK uses singleton pattern, but ensure you're not calling
initialize()multiple times - Use
autoInitialize: falseinuseSession()if you want manual control
401 errors after browser close and reopen
- The SDK automatically clears stale cookies and re-bootstraps on fresh page loads
- Both
fetchInterceptorandsetupAxiosInterceptorcallensureReady()before every request, which waits for bootstrap to complete - If using a manual fetch wrapper (Approach C), ensure you call
await sm.ensureReady()before each request - Verify your 401 handler compares against
sm.config.invalidSessionError(default:'INVALID-GW-SESSION'), not a hardcoded string
Next.js SSR errors
- The SDK detects SSR environments and prevents initialization
- Always wrap initialization in
useEffector client components ('use client') - Do not call
initialize()during server-side rendering
License
MIT
