🚪 Dashboard Gateway
📖 Overview
The Dashboard Gateway is the main entry point for all DashClicks dashboard/frontend requests. It serves as a session-based authentication gateway that converts browser sessions into JWT tokens for backend services.
Core Responsibilities:
- Session Management: Manage browser sessions with automatic renewal
- Authentication Conversion: Convert
x-session-idheader to JWT Bearer token - HTTP Proxying: Forward REST API requests to API Router (port 5001)
- WebSocket Proxying: Upgrade and forward WebSocket connections to General Socket (port 4000)
- Token Refresh: Automatically refresh expired JWT tokens using refresh tokens
- Last Seen Tracking: Update user's last seen timestamp
Architecture: Express HTTP Proxy + WebSocket Proxy + Session Management
Port: 5000 (configurable via PORT environment variable)
🏗️ Architecture
System Architecture
graph TB
subgraph "Client Layer"
BROWSER[Browser/Frontend<br/>React Dashboard]
end
subgraph "Gateway Layer - Port 5000"
DG[Dashboard Gateway<br/>Session Management]
end
subgraph "Backend Services"
ROUTER[API Router<br/>Port 5001]
INTERNAL[Internal API<br/>Port 5002]
EXTERNAL[External API<br/>Port 5003]
SOCKET[General Socket<br/>Port 4000]
end
subgraph "Data Layer"
MONGO[(MongoDB<br/>Sessions)]
end
BROWSER -->|x-session-id| DG
DG -->|Bearer JWT| ROUTER
DG -->|Bearer JWT| SOCKET
ROUTER --> INTERNAL
ROUTER --> EXTERNAL
DG <--> MONGO
style DG fill:#f9f,stroke:#333,stroke-width:4px
Request Flow Architecture
sequenceDiagram
participant Browser
participant Gateway as Dashboard Gateway<br/>(Port 5000)
participant Session as ApiSession<br/>(MongoDB)
participant OAuth as OAuth Token<br/>Endpoint
participant Router as API Router<br/>(Port 5001)
participant API as Internal/External<br/>API
Browser->>Gateway: HTTP Request<br/>(x-session-id header)
Gateway->>Session: Find session by ID
Session-->>Gateway: Session data
alt Session Expired
Gateway->>Session: Delete session
Gateway-->>Browser: 401 SESSION_EXPIRED
else Session Valid but Token Expired
Gateway->>OAuth: POST /oauth/token<br/>(refresh_token)
OAuth-->>Gateway: New access_token
Gateway->>Session: Update token & expiration
else Session Valid
Note over Gateway: Session < 12hrs until expiry?
Gateway->>Session: Renew session expiration
end
Gateway->>Router: Proxy request<br/>(Bearer JWT token)
Router->>API: Process request
API-->>Router: Response
Router-->>Gateway: Response
Gateway-->>Browser: Response
🔧 Core Components
Express Server Setup
File: dashboard-gateway/app.js
Initialization:
const express = require('express');
const httpProxy = require('http-proxy');
const app = express();
const server = http.createServer(app);
// Proxy server with WebSocket support
const apiProxy = httpProxy.createProxyServer({ ws: true });
const apiServer = `${process.env.API_BASE_URL}`; // http://router:5001
Middleware Stack:
app.set('trust proxy', true);
app.set('etag', false);
app.use(nocache());
app.use(
cors({
origin: (origin, callback) => callback(null, true),
credentials: true,
}),
);
app.use(cookieParser());
Configuration:
- Trust proxy headers (for X-Forwarded-* headers)
- Disable ETag caching
- CORS enabled with credentials
- Cookie parsing for session cookies
- No cache headers to prevent sensitive data caching
Session Management
Session Model: ApiSession
Schema:
{
_id: ObjectId, // Session ID (sent in x-session-id header)
user_id: ObjectId, // User reference
account_id: ObjectId, // Account reference
token: {
access_token: String, // JWT access token
refresh_token: String, // OAuth refresh token
token_type: 'Bearer'
},
session_expiration: Date, // When session expires (24 hours default)
token_expiration: Date, // When JWT token expires (shorter-lived)
created_at: Date
}
Session Sources: The gateway accepts session ID from three sources:
- Header:
x-session-id(recommended, most secure) - Cookie:
sid_dc_sw(fallback for browser environments) - Query Parameter:
?token=(limited use cases, WebSocket connections)
Session Lifecycle:
- Creation: Created by Internal API on successful login
- Usage: Sent by browser with every request
- Renewal: Auto-renewed when < 12 hours until expiration
- Refresh: JWT token refreshed when expired using refresh token
- Expiration: Deleted when session expires (no renewal)
- Logout: Explicitly deleted by Internal API on user logout
Session-to-JWT Conversion
Purpose: Bridge session-based frontend authentication with JWT-based backend authentication
Why This Pattern:
- Frontend Expectation: React dashboard uses session-based authentication (familiar pattern)
- Backend Security: Microservices use JWT for stateless authentication
- Best of Both Worlds: Users get persistent sessions, services get secure tokens
Conversion Process:
// 1. Extract session ID from multiple sources
const sessionId = req.headers['x-session-id'] || req.cookies.sid_dc_sw || req.query.token;
// 2. Load session from database
const session = await ApiSession.findById(sessionId);
// 3. Validate session not expired
if (new Date(session.session_expiration) <= new Date()) {
// Session expired - cleanup and reject
await ApiSession.findByIdAndDelete(session.id);
await ApiRefreshToken.findOneAndDelete({
refresh_token: session.token.refresh_token,
});
return res.status(401).json({
success: false,
errno: 401,
message: 'SESSION_EXPIRED',
});
}
// 4. Extract JWT token
const jwtToken = session.token.access_token;
// 5. Forward request with Bearer token
apiProxy.web(req, res, {
target: apiServer,
headers: {
Authorization: `Bearer ${jwtToken}`,
'x-dc-trace': req.headers['cf-ray'] || '',
},
});
Automatic Session Renewal
Trigger: When session expiration is less than 12 hours away
Why 12 Hours:
- Ensures active users never experience session expiration
- Provides buffer time before actual expiration
- Balances security (24-hour limit) with user experience (auto-renewal)
Renewal Logic:
const app = await getApiApp({ client_id: req.headers['x-client-id'] });
// Calculate 12-hour threshold
const threshold = new Date().getTime() + 60 * 60 * (app.dashclicks.session_exp * 8) * 1000;
// Check if session expires within 12 hours
if (new Date(session.session_expiration).getTime() <= threshold) {
// Renew for another 24 hours
const newExpiration = new Date().getTime() + 60 * 60 * (app.dashclicks.session_exp * 24) * 1000;
await ApiSession.findByIdAndUpdate(session.id, {
session_expiration: new Date(newExpiration),
});
}
Configuration:
app.dashclicks.session_exp= 1 (configurable per OAuth app)session_exp * 8 * 60 * 60 * 1000= 8 hours in milliseconds (threshold check)session_exp * 24 * 60 * 60 * 1000= 24 hours in milliseconds (renewal duration)
Automatic Token Refresh
Trigger: When JWT token expiration date has passed
Why Separate from Session:
- JWT tokens are shorter-lived for security
- Session can remain valid while token expires
- Refresh token used to obtain new access token
OAuth Refresh Flow:
if (new Date(session.token_expiration) <= new Date()) {
// Token expired - use refresh token to get new one
const tokenResponse = await axios.post(
`${process.env.API_BASE_URL}/v1/auth/oauth/token`,
{
grant_type: 'refresh_token',
refresh_token: session.token.refresh_token,
},
{
headers: {
Authorization: `Basic ${Buffer.from(`${app.id}:${app.secret}`).toString('base64')}`,
},
},
);
const newToken = tokenResponse.data;
const newTokenExpiration = new Date().getTime() + newToken.expires_in * 1000;
// Update session with new token
await ApiSession.findByIdAndUpdate(session.id, {
'token.access_token': newToken.access_token,
token_expiration: new Date(newTokenExpiration),
});
}
OAuth Grant Type: refresh_token
- Standard OAuth 2.0 grant type
- Uses refresh token to obtain new access token
- No user interaction required
- Basic authentication with app credentials
🌐 HTTP Request Proxying
Proxy Target: API Router (Port 5001)
All Routes Middleware:
app.all('/*', async (req, res, next) => {
// Session management, token refresh, and proxying logic
});
Request Processing Flow:
- Extract Session: Get session ID from headers/cookies/query
- Load Session: Fetch session from MongoDB
- Check Expiration: Validate session hasn't expired
- Renew Session: Auto-renew if < 12 hours until expiration
- Refresh Token: Get new JWT if token expired
- Update Last Seen: Track user activity
- Proxy Request: Forward to API Router with Bearer token
- Return Response: Proxy response back to client
Headers Added to Proxied Request:
{
'Authorization': `Bearer ${token}`, // JWT access token
'x-dc-trace': req.headers['cf-ray'] || '' // Cloudflare trace ID for request tracking
}
Parameter Replacement:
If session ID is passed as query parameter (?token=sessionId), it's replaced with the actual JWT token:
// Before: ?token=507f1f77bcf86cd799439011
// After: ?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Special Endpoints
Health Check
Endpoint: GET /status
Purpose: Service health monitoring
Response:
{
status: 'ok';
}
No Proxy: Handled directly by gateway, not forwarded
Funnel Webhook Test
Endpoint: POST /funnel_webhooks/test
Purpose: Test endpoint for funnel webhook integrations
Response:
{
time: '2025-10-13T...'; // UTC timestamp
}
// Headers:
// X-Clickfunnels-Webhook-Delivery-Id: [hash]
No Proxy: Handled directly by gateway
Last Seen Tracking
Purpose: Track user activity for presence and analytics
Implementation:
if (session) {
try {
await User.findByIdAndUpdate(session.user_id, {
last_seen: new Date(),
});
} catch (err) {
logger.error({
initiator: 'dashboard-gateway',
error: err,
message: 'Failed to update last seen time',
});
}
}
Updated On: Every HTTP request (not WebSocket connections)
Non-Blocking: Errors logged but don't block request
🔌 WebSocket Proxying
Proxy Target: General Socket (Port 4000)
Purpose: Enable real-time features (live chat, notifications, updates)
WebSocket Upgrade Handler
Event: server.on('upgrade', async (req, socket, head) => { ... })
WebSocket Upgrade Flow:
sequenceDiagram
participant Browser
participant Gateway as Dashboard Gateway
participant Session as ApiSession (DB)
participant Socket as General Socket<br/>(Port 4000)
Browser->>Gateway: WebSocket Upgrade Request<br/>(?session_id=...)
alt No Session ID
Gateway-->>Browser: 401 Web Socket Protocol Handshake
else Session ID Present
Gateway->>Session: Load session
alt Session Valid
Note over Gateway: Renew/refresh if needed
Gateway->>Socket: Upgrade & Proxy<br/>(x-token header)
Socket-->>Gateway: Connection established
Gateway-->>Browser: WebSocket connection ready
else Session Invalid
Gateway-->>Browser: Connection rejected
end
end
Authentication Check:
// Reject if no session ID or explicitly undefined
if (!req.query.session_id || req.query.session_id == 'undefined') {
return socket.write(
'HTTP/1.1 401 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n\r\n',
);
}
Session Processing: Same logic as HTTP requests:
- Load session from database
- Check expiration and renew if needed
- Refresh token if expired
- Extract JWT token
Proxy Configuration:
apiProxy.ws(req, socket, head, {
target: process.env.GENERAL_SOCKET || 'ws://bk-general-socketio',
changeOrigin: true,
headers: {
'x-token': token || '', // JWT token for authentication
'x-dc-trace': req.headers['x-dc-trace'], // Request tracing
},
});
Target URL:
- Environment variable:
GENERAL_SOCKET - Default:
ws://bk-general-socketio(Docker service name) - Port: 4000
Token Passing: JWT token passed in x-token header (not Authorization for WebSocket compatibility)
🚨 Error Handling
Session Expired Error
Condition: session.session_expiration <= new Date()
Response:
{
success: false,
errno: 401,
message: 'SESSION_EXPIRED'
}
Cleanup Actions:
// Delete session
await ApiSession.findByIdAndDelete(session.id);
// Delete refresh token
await ApiRefreshToken.findOneAndDelete({
refresh_token: session.token.refresh_token,
});
Client Behavior: Should redirect to login page
Token Refresh Failed
Causes:
- Refresh token expired or invalid
- OAuth endpoint unavailable
- App credentials invalid
Impact: Request fails with 401/500 error
Resolution: User must re-login to obtain new session
Logging:
logger.error({
initiator: 'dashboard-gateway',
error: err,
req,
});
Proxy Errors
Handler:
apiProxy.on('error', (error, req, res) => {
let errorCode = error.errno || 400;
let errorMessage = error.message;
let additional_info = error.toString();
if (error.isAxiosError) {
errorCode = error.response?.status;
errorMessage = error.response?.data?.message || errorMessage;
additional_info = { ...error.toJSON(), stack: null, config: null };
}
logger.error({ initiator: 'dashboard-gateway', error, req });
res.end(
JSON.stringify({
status: false,
errno: errorCode,
message: errorMessage,
additional_info,
}),
);
});
Common Proxy Errors:
- ECONNREFUSED: Backend service down
- ETIMEDOUT: Backend service not responding
- ENOTFOUND: DNS resolution failed
Global Error Handler
Middleware: app.use((error, req, res, next) => { ... })
Handles:
- Axios errors (from token refresh API calls)
- General application errors
- Unhandled promise rejections
- Express middleware errors
Response Format:
{
status: false,
errno: errorCode,
message: errorMessage,
additional_info: additionalInfo
}
🔒 Security Features
Session Security
Session ID Storage Options:
- Header:
x-session-id✅ Most secure, recommended - Cookie:
sid_dc_sw⚠️ Fallback, subject to cookie vulnerabilities - Query:
?token=⚠️ Exposed in URLs, limited use cases
Session Duration: 24 hours (configurable via OAuth app settings)
Token Duration: Shorter-lived than session (typically 1-2 hours)
Session Binding:
- Session bound to specific user and account
- Cannot be transferred between users
- Invalidated on logout
Two-Level Authentication
Level 1: Session
- Purpose: User experience (persistent login)
- Duration: 24 hours
- Storage: MongoDB
- Renewable: Yes (auto-renewed)
Level 2: JWT Token
- Purpose: Security (stateless auth)
- Duration: Shorter (1-2 hours)
- Storage: Within session object
- Renewable: Yes (via refresh token)
Why Two Levels:
- Compromise of token doesn't compromise entire session
- Token can expire and refresh without user interaction
- Session provides long-term persistence
- Token provides short-term security
CORS Configuration
Policy: Allow all origins with credentials
app.use(
cors({
origin: (origin, callback) => {
callback(null, true); // Allow all origins
},
credentials: true,
}),
);
Credentials: Enabled to support cookies and authentication headers
Why Permissive: Gateway handles authentication, not CORS policy
Security Headers
Helmet Middleware: Adds security headers
app.use(helmet());
Headers Added:
X-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINX-XSS-Protection: 1; mode=block- And more security-focused headers
No Cache Middleware:
app.use(nocache());
Purpose: Prevent caching of sensitive data
Headers Set:
Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidatePragma: no-cacheExpires: 0Surrogate-Control: no-store
Request Tracing
Cloudflare Ray ID: Captured for request tracking
req.headers['x-dc-trace'] = req.headers['cf-ray'] || '';
Forwarded to Backend: All proxied requests include trace ID
Purpose:
- Debug request flow across services
- Track errors through microservices
- Correlate logs across services
📊 User Activity Tracking
Last Seen Update
Triggered On: Every HTTP request (excluding WebSocket)
Implementation:
if (session) {
try {
await User.findByIdAndUpdate(session.user_id, {
last_seen: new Date(),
});
} catch (err) {
logger.error({
initiator: 'dashboard-gateway',
error: err,
message: 'Failed to update last seen time',
});
}
}
Non-Blocking: Errors don't block request processing
Use Cases:
- Presence: Show online/offline status
- Analytics: Track user activity patterns
- Security: Detect unusual access patterns
- Support: Help identify active users
🔗 Integration with Other Services
API Router (Port 5001)
Relationship: All HTTP requests proxied to router
Request Flow:
Browser → Gateway (session auth) → Router (JWT auth) → Internal/External API
Communication:
- Protocol: HTTP/HTTPS
- Method: Reverse proxy
- Authentication: Bearer JWT token
General Socket (Port 4000)
Relationship: WebSocket connections proxied to socket server
Connection Flow:
Browser → Gateway (session auth) → General Socket (JWT auth) → Real-time events
Communication:
- Protocol: WebSocket (ws://)
- Method: WebSocket proxy with upgrade
- Authentication: x-token header
Internal API: OAuth Endpoints
Relationship: Token refresh via OAuth 2.0 flow
Endpoint: POST /v1/auth/oauth/token
Grant Type: refresh_token
Request:
{
grant_type: 'refresh_token',
refresh_token: '...'
}
// Headers:
// Authorization: Basic base64(clientId:clientSecret)
Response:
{
access_token: 'eyJhbGci...',
token_type: 'Bearer',
expires_in: 3600 // seconds
}
MongoDB
Collections Used:
apisessions- Session storageapirefreshtokens- Refresh token storageusers- User data and last_seen updates
Connection: Via utilities/db.js (shared utility)
🎯 Common Patterns
Successful Request Flow
// 1. Client sends request
GET /v1/accounts/me
Headers: x-session-id: 507f1f77bcf86cd799439011
// 2. Gateway processes
- Loads session from DB
- Checks expiration
- Renews if needed
- Extracts JWT token
// 3. Gateway proxies to router
GET /v1/accounts/me
Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
// 4. Router forwards to Internal API
// 5. Response flows back through chain
Session Renewal Flow
// Condition: Session expires in < 12 hours
const currentTime = Date.now();
const sessionExpiry = session.session_expiration.getTime();
const twelveHours = 12 * 60 * 60 * 1000;
if (sessionExpiry - currentTime < twelveHours) {
// Extend session for another 24 hours
await ApiSession.findByIdAndUpdate(session.id, {
session_expiration: new Date(currentTime + 24 * 60 * 60 * 1000),
});
}
Token Refresh Flow
// Condition: Token expired
if (new Date(session.token_expiration) <= new Date()) {
// Call OAuth endpoint
const response = await axios.post(
`${API_BASE_URL}/v1/auth/oauth/token`,
{ grant_type: 'refresh_token', refresh_token: session.token.refresh_token },
{ headers: { Authorization: `Basic ${basicAuth}` } },
);
// Update session with new token
await ApiSession.findByIdAndUpdate(session.id, {
'token.access_token': response.data.access_token,
token_expiration: new Date(Date.now() + response.data.expires_in * 1000),
});
}
📈 Performance Considerations
Database Queries Per Request
Minimum: 2 queries
ApiSession.findById()- Load sessionUser.findByIdAndUpdate()- Update last seen
With Renewal: +1 query 3. ApiSession.findByIdAndUpdate() - Update session expiration
With Token Refresh: +2 queries 3. OAuth API call (HTTP request, not DB) 4. ApiSession.findByIdAndUpdate() - Update token
Caching Opportunities
Not Implemented (by design):
- Session caching would break multi-instance deployments
- Token caching could serve stale tokens
Why No Caching:
- Sessions must be fresh across all gateway instances
- Token expiration must be checked in real-time
- MongoDB is fast enough for session lookups
Graceful Shutdown
SIGTERM Handler:
process.on('SIGTERM', () => {
logger.log({
initiator: 'dashboard-gateway',
message: 'SIGTERM signal received. Not accepting new connections.',
});
server.close(() => {
logger.log({ initiator: 'dashboard-gateway', message: 'Server closed' });
process.exit(0);
});
});
Process:
- Stop accepting new connections
- Wait for existing requests to complete
- Close server
- Exit process
🐛 Troubleshooting
Issue: SESSION_EXPIRED Errors
Symptoms: Users getting logged out unexpectedly
Possible Causes:
- Session actually expired (> 24 hours)
- Session deleted by logout endpoint
- Database connection issues
- Session collection cleared
Debug Steps:
# Check if session exists in database
db.apisessions.findOne({ _id: ObjectId("507f1f77...") })
# Check session expiration
{
_id: ObjectId("..."),
session_expiration: ISODate("2025-10-14T..."), # Check if past current time
token_expiration: ISODate("2025-10-13T...")
}
Resolution:
- If session legitimately expired: User must login again
- If database issue: Check MongoDB connection
- If unexpected deletion: Check for logout endpoint calls
Issue: Token Refresh Failures
Symptoms: Requests failing even with valid session
Possible Causes:
- Refresh token expired or invalid
- OAuth endpoint unavailable
- App credentials incorrect
Debug Steps:
# Check refresh token exists
db.apirefreshtokens.findOne({ refresh_token: "..." })
# Test OAuth endpoint manually
curl -X POST http://router:5001/v1/auth/oauth/token \
-H "Authorization: Basic $(echo -n 'clientId:secret' | base64)" \
-d '{"grant_type":"refresh_token","refresh_token":"..."}'
Resolution:
- If refresh token invalid: User must login again
- If OAuth endpoint down: Check Internal API health
- If credentials wrong: Verify OAuth app configuration
Issue: Proxy Connection Errors
Symptoms: 502/504 errors, ECONNREFUSED
Possible Causes:
- API Router service down
- Network connectivity issues
- Service not listening on port 5001
Debug Steps:
# Check if router is running
docker ps | grep router
# Check if port is open
nc -zv router 5001
# Check gateway logs
docker logs dashboard-gateway | grep error
Resolution:
- If router down: Restart router service
- If network issue: Check Docker network configuration
- If port not open: Verify router port configuration
Issue: WebSocket Connection Failures
Symptoms: Real-time features not working
Possible Causes:
- Session ID not in query parameters
- General Socket service down
- WebSocket upgrade failing
Debug Steps:
# Check WebSocket connection in browser console
const ws = new WebSocket('ws://localhost:5000?session_id=...');
ws.onerror = (e) => console.error(e);
# Check general-socket service
docker ps | grep general-socket
nc -zv general-socket 4000
Resolution:
- If no session ID: Check client WebSocket URL
- If socket down: Restart general-socket service
- If upgrade failing: Check proxy configuration
📊 Monitoring & Metrics
Key Metrics to Monitor
Request Metrics:
- Total requests per minute
- Session lookup time
- Token refresh rate
- Proxy latency
Error Metrics:
- SESSION_EXPIRED rate
- Token refresh failures
- Proxy connection errors
- Database query failures
Session Metrics:
- Active sessions count
- Session renewal rate
- Average session duration
- Sessions per user
Health Check Endpoint
Endpoint: GET /status
Expected Response:
{
status: 'ok';
}
HTTP Status: 200
Use Cases:
- Load balancer health checks
- Kubernetes liveness probes
- Monitoring system checks
Logging
Log Format: Structured JSON logging via utilities/logger.js
Key Log Messages:
// Startup
{ initiator: 'dashboard-gateway', message: 'Express server running on port 5000' }
// Errors
{ initiator: 'dashboard-gateway', error: err, message: '...', req }
// Shutdown
{ initiator: 'dashboard-gateway', message: 'SIGTERM signal received. ...' }
Log Levels: info, error (via logger utility)
🔗 Related Documentation
- General Socket Service - Real-time WebSocket events
- Internal API: Authentication - Login and OAuth endpoints
- Internal API: OAuth - OAuth 2.0 implementation
📈 Statistics
Service Type: HTTP Proxy + WebSocket Proxy + Session Management
Port: 5000
Proxies To: API Router (5001), General Socket (4000)
Session Storage: MongoDB (apisessions collection)
Default Session Duration: 24 hours (configurable per OAuth app)
Session Renewal Threshold: 12 hours
Token Refresh: Automatic via OAuth refresh_token grant
WebSocket Support: Yes (upgrade and proxy to General Socket)
Request Tracing: Cloudflare Ray ID (x-dc-trace header)
Last Seen Tracking: Updated on every HTTP request
Status: ✅ Core Service - Main entry point for all frontend requests
Maintainer: Backend Team
Dependencies: MongoDB, API Router, General Socket, Internal API (OAuth)