Skip to main content

🚪 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-id header 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:

  1. Header: x-session-id (recommended, most secure)
  2. Cookie: sid_dc_sw (fallback for browser environments)
  3. Query Parameter: ?token= (limited use cases, WebSocket connections)

Session Lifecycle:

  1. Creation: Created by Internal API on successful login
  2. Usage: Sent by browser with every request
  3. Renewal: Auto-renewed when < 12 hours until expiration
  4. Refresh: JWT token refreshed when expired using refresh token
  5. Expiration: Deleted when session expires (no renewal)
  6. 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:

  1. Extract Session: Get session ID from headers/cookies/query
  2. Load Session: Fetch session from MongoDB
  3. Check Expiration: Validate session hasn't expired
  4. Renew Session: Auto-renew if < 12 hours until expiration
  5. Refresh Token: Get new JWT if token expired
  6. Update Last Seen: Track user activity
  7. Proxy Request: Forward to API Router with Bearer token
  8. 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:

  1. Load session from database
  2. Check expiration and renew if needed
  3. Refresh token if expired
  4. 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:

  1. Header: x-session-id ✅ Most secure, recommended
  2. Cookie: sid_dc_sw ⚠️ Fallback, subject to cookie vulnerabilities
  3. 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: nosniff
  • X-Frame-Options: SAMEORIGIN
  • X-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-revalidate
  • Pragma: no-cache
  • Expires: 0
  • Surrogate-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 storage
  • apirefreshtokens - Refresh token storage
  • users - 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

  1. ApiSession.findById() - Load session
  2. User.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:

  1. Stop accepting new connections
  2. Wait for existing requests to complete
  3. Close server
  4. Exit process

🐛 Troubleshooting

Issue: SESSION_EXPIRED Errors

Symptoms: Users getting logged out unexpectedly

Possible Causes:

  1. Session actually expired (> 24 hours)
  2. Session deleted by logout endpoint
  3. Database connection issues
  4. 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:

  1. Refresh token expired or invalid
  2. OAuth endpoint unavailable
  3. 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:

  1. API Router service down
  2. Network connectivity issues
  3. 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:

  1. Session ID not in query parameters
  2. General Socket service down
  3. 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)

📈 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)

💬

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:30 AM