Advanced Configuration
Overview
This guide covers production configuration, external service integration, and deployment strategies for QuranBot.
- Environment variable management
- Lavalink node configuration
- Redis + Firebase setup
- PM2 deployment & scaling
Environment Variables
QuranBot uses a dual-environment system with development.env and production.env.
The active environment is determined by NODE_ENV in the base .env file.
Core Variables (Required)
| Variable | Description | Example |
|---|---|---|
DISCORD_TOKEN |
Bot authentication token from Discord Developer Portal | MTEyMz...xyz |
CLIENT_ID |
Discord application client ID (snowflake) | 1505646575962292314 |
SPE_USER_ID |
Comma-separated list of developer user IDs with admin access | 123456789,987654321 |
BACKUP_INTERVAL_MS |
Interval for automatic Firebase backups (milliseconds) | 600000 (10 minutes) |
Environment-Specific Variables
| Variable | Development | Production |
|---|---|---|
DEVELOPMENT_SERVER_ID |
Test server ID for backup channel | Not used |
DEVELOPMENT_CHANNEL_ID |
Channel ID for receiving backup files | Not used |
PRODUCTION_SERVER_ID |
Not used | Production server ID for backups |
PRODUCTION_CHANNEL_ID |
Not used | Production backup channel ID |
TOPGG_TOKEN |
Removed automatically | top.gg API token for bot listing |
In development mode, TOPGG_TOKEN is automatically removed from process.env to
prevent accidental use of production API keys during testing. Never commit real tokens to version
control.
Environment Switcher
The src/config/envSwitcher.js module handles environment selection at startup:
require('pathlra-aliaser')();
const path = require('path');
const dotenv = require('dotenv');
const logger = require('@logging/logger');
const fs = require('fs');
const baseEnvFilePath = path.resolve(__dirname, '../../.env');
let baseEnvConfig = {};
if (fs.existsSync(baseEnvFilePath)) {
const rawBaseEnv = fs.readFileSync(baseEnvFilePath, 'utf8');
baseEnvConfig = dotenv.parse(rawBaseEnv);
}
const activeEnv = baseEnvConfig.NODE_ENV;
const targetedEnvPath = path.resolve(__dirname, `../../${activeEnv}.env`);
const loadResult = dotenv.config({ path: targetedEnvPath });
if (loadResult.error) {
logger.error(`Could Not Load ${activeEnv} Env File`, loadResult.error.message);
process.exit(1);
} else {
logger.info(`Loaded ${activeEnv} Env`);
}
// Remove TOPGG_TOKEN in development to prevent accidental use of production API key during dev
if (activeEnv === 'development') {
if (process.env.TOPGG_TOKEN) {
delete process.env.TOPGG_TOKEN;
logger.info('Development Mode TOPGG_TOKEN Removed');
}
}
module.exports.isDevelopment = activeEnv === 'development';
module.exports.isProduction = activeEnv === 'production';
module.exports.currentEnv = activeEnv;
Setup Steps:
-
Copy
development.env.example→development.env - Copy
production.env.example→production.env - Set
NODE_ENV=production(ordevelopment) in base.env
Lavalink Configuration
QuranBot uses Lavalink v4 for scalable, low-latency audio processing. Configuration is managed via
LAVALINK_NODES_ environment variable.
JSON Format (Recommended)
# Lavalink Audio Server
LAVALINK_NODES_='[
{
"host": "ip",
"port": 2022,
"password": "123",
"secure": false,
"maxPlayers": 60,
"location": "India, Pune",
"flag": "🇮🇳",
"playerCreateDelay": 100
},
{
"host": "ip",
"port": 2022,
"password": "123",
"secure": false,
"maxPlayers": 500,
"location": "Mexico, Santiago de Querétaro",
"flag": "🇲🇽",
"playerCreateDelay": 100
}
]'
Node Selection Logic
The bot selects nodes using getBestNode() in src/startup/botSetup.js:
- Filter nodes: connected + under
maxPlayerslimit - Sort by current player count (least loaded first)
- Return first available node;
nullif all full
Key Parameters:
maxPlayers: Max concurrent players per node (prevents overload)playerCreateDelay: Artificial delay (ms) for load distributionsecure: Use HTTPS/WSS for node communicationlocation/flag: Display metadata for admin UI
Node Management & Failover
Runtime Node Switching
Users can manually select nodes via the control panel. The select_lavalink_node menu handler in
src/interactions/menus/lavalinkNodes.js manages migration:
// Key steps during node switch:
const currentPosition = guildState.player.position || 0;
const wasPaused = guildState.player.paused || false;
// 1. Reconnect to new node
await initializeConnection(guildId, guildState, targetChannel, adapter);
// 2. Recreate audio resource
const track = await createSurahResource(guildState, index);
// 3. Resume playback at exact position
await guildState.player.play({ track });
if (currentPosition > 0) {
await guildState.player.seek(currentPosition);
}
if (wasPaused) await guildState.player.pause(true);
Automatic Failover
If a node disconnects during playback:
-
Lavalink client auto-reconnects per
retryAmount/retryDelay - Bot listens for
nodeManager.on('disconnect')events - State persistence ensures playback resumes after reconnection
For backward compatibility, the bot still supports numbered variables like
LAVALINK_NODE_1_HOST, but the JSON format is recommended for maintainability.
Redis Setup
Redis provides hot caching for guild states, enabling stateless sharding and sub-millisecond lookups.
Configuration
| Variable | Default | Description |
|---|---|---|
REDIS_URL |
redis://127.0.0.1:6379 |
Redis connection string (supports auth, TLS) |
Connection String Examples
# Local development
REDIS_URL=redis://localhost:6379
# With password authentication
REDIS_URL=redis://:my_password@localhost:6379
# Redis Cloud / Managed service
REDIS_URL=rediss://default:password@redis-12345.c1.us-east-1-2.ec2.redns.redis-cloud.com:12345
# With database selection
REDIS_URL=redis://localhost:6379/2
async function get(key) {
try {
if (clientManager.isRedisReady && client) {
const result = await client.get(key);
return result ? JSON.parse(result) : result;
}
} catch (error) {
logger.error(`Redis get failed: ${error.message}`);
}
// Fallback to memory
return memoryFallbackMap.get(key) || null;
}
Firebase Integration
Firebase Realtime Database serves as the cold storage layer for persistent state, complaints, and statistics.
Service Account Setup
- Go to Firebase Console → Project Settings → Service Accounts
- Generate new private key → Download JSON file
- Extract values into environment variables (see table below)
Required Firebase Variables
| Variable | Source in JSON |
|---|---|
FIREBASE_ADMIN_TYPE |
type |
FIREBASE_ADMIN_PROJECT_ID |
project_id |
FIREBASE_ADMIN_PRIVATE_KEY |
private_key (see note below) |
FIREBASE_ADMIN_PRIVATE_KEY_ID |
private_key_id |
FIREBASE_ADMIN_CLIENT_EMAIL |
client_email |
FIREBASE_ADMIN_CLIENT_ID |
client_id |
FIREBASE_DATABASE_URL |
https://your-project.firebaseio.com |
The FIREBASE_ADMIN_PRIVATE_KEY value must preserve newlines. In .env files,
wrap the key in quotes and escape newlines as \n, or use a multi-line string if your
environment supports it. The bot's getSanitizedPrivateKey() function handles common
formatting issues.
Database Rules
Ensure your firebase.rules.json denies public access:
{
"rules": {
".read": false,
".write": false
}
}
The bot uses server-side firebase-admin with service account credentials, which bypasses these
rules for authenticated operations.
Deployment with PM2
QuranBot is optimized for process management via PM2, supporting auto-restart, logging, and clustering.
PM2 Configuration (ecosystem.config.js)
module.exports = {
apps: [{
name: 'QuranBot',
script: 'src/bot/core.js',
node_args: '--trace-warnings --trace-deprecation --unhandled-rejections=strict --enable-source-maps',
autorestart: true,
watch: false,
ignore_watch: ['node_modules', 'storage/logs', 'logs', '.git', '.test'],
instances: 1,
exec_mode: 'fork',
env: {
merge_logs: true,
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
error_file: 'storage/pm2/logs/pm2-error.log',
out_file: 'storage/pm2/logs/pm2-out.log',
restart_delay: 7000,
},
}],
};
Deployment Commands
| Command | Description |
|---|---|
pnpm run pm2:start |
Start bot with PM2 using ecosystem.config.js |
pnpm run pm2:logs |
View last 500 lines of PM2 logs |
pnpm run pm2:restart |
Graceful restart with 7s delay |
pnpm run pm2:stop |
Stop the bot process |
pnpm run pm2:delete |
Remove bot from PM2 process list |
Production Startup Script
#!/bin/bash
# deploy.sh - Production deployment script
set -e
# Load environment
export NODE_ENV=production
source .env
# Install dependencies
pnpm install --prod
# Run database migrations if needed
# pnpm run migrate
# Start with PM2
pnpm run pm2:start
# Save PM2 process list for auto-start on reboot
pm2 save
echo "QuranBot deployed successfully"
Health Checks & Monitoring
Built-in Health Endpoint
The bot exposes a lightweight HTTP health check on port HH_CH_PORT (default:
3000):
# Example request
curl http://localhost:3000/health
# Response
{
"status": "ok",
"voiceConnections": 42,
"uptime": 86400.123,
"memory": {
"rss": 256000000,
"heapTotal": 128000000,
"heapUsed": 96000000
},
"timestamp": "2026-05-24T12:00:00.000Z"
}
Key Metrics to Monitor
-
Memory Usage: Alert if
heapUsed> 1.5GB (triggers aggressive GC) - Voice Connections: Sudden drops may indicate Lavalink issues
-
Uptime: Track restarts via
pm2 logsor health endpoint - Log Volume: Spike in
errorlogs may indicate external API failures
Log Rotation
Logs are automatically archived daily at Cairo midnight:
- Location:
storage/logs/archive/ - Format:
logs-general-YYYY-MM-DD.zip - Retention: 60 days (configurable in
logging_config)
Troubleshooting Guide
Bot fails to start with "Firebase Private Key Format Invalid"
This indicates newline escaping issues in the .env file. Solutions:
- Use
echo -n "$PRIVATE_KEY" | base64to encode, then decode at runtime - Wrap the key in triple quotes if your shell supports it
- Use a secrets manager (e.g., HashiCorp Vault) instead of env files
The bot's getSanitizedPrivateKey() handles common cases, but complex keys may require
preprocessing.
Lavalink nodes show "No compatible encryption modes"
This Discord voice encryption mismatch occurs when:
- Lavalink version is incompatible with Discord's current encryption suite
- Node is behind a proxy that strips TLS headers
- Bot and Lavalink have mismatched
securesettings
Verify Lavalink is v4.x and secure matches your deployment (false for HTTP, true for
HTTPS).
High memory usage after several days
The bot includes automatic memory management:
- Every 5 minutes: Check if heap > 250MB → trigger GC
- Every hour: Clean destroyed voice connections from state maps
- On interaction cache TTL expiry: Remove stale entries
If memory still grows, check for:
- Large guilds with many channels (state maps scale linearly)
- Long-running intervals without cleanup
- Event listeners not properly removed on guild leave
Use /سرعة command to view real-time memory stats.
Diagnostic Commands
| Command | Use Case |
|---|---|
/سرعة |
View bot latency, uptime, memory, and Lavalink node status |
pnpm run pm2:logs --lines 100 |
Inspect recent logs for error patterns |
redis-cli ping |
Verify Redis connectivity |
curl https://your-lavalink-host/v4/stats |
Check Lavalink node health directly |