Node.js REST API for DX Cluster Spots with POTA, SOTA, and RBN Integration
Aggregates spots from multiple DX Clusters, POTA, SOTA, and Reverse Beacon Network into a unified REST API with DXCC lookups via Wavelog.
A public test instance is available for testing and development purposes:
- URL: https://jo30.de/dxcluster-per-rest-json/
- Cache: 1000 spots
- Purpose: Testing and development only
- More info: jo30.de/dxcluster-per-rest-json/
- Live Test Instance
- Requirements
- Installation
- Configuration
- Deployment Modes
- API Endpoints
- Module Configuration
- Development
- Troubleshooting
- Node.js 16+ (18+ recommended for native fetch support)
- Bun 1.0+ (for Docker mode) or NPM (for Native/Passenger modes)
- Docker & Docker Compose (optional, for Docker mode)
- Phusion Passenger (optional, for shared hosting)
git clone https://github.com/int2001/DXClusterAPI.git
cd DXClusterAPIUsing npm:
npm installUsing Bun:
bun installCopy the sample environment file and edit it:
cp .env.sample .envEdit .env with your settings (see Configuration section).
All configuration is done via the .env file. Copy .env.sample to .env and adjust:
# Runtime mode: docker, native, or passenger
MODE=native
# Web server port (ignored in Passenger mode)
WEBPORT=3000
# Base URL path (empty for root, or "/api" for subpath)
BASEURL=
# Maximum spots to cache (default: 500)
MAXCACHE=500# Single cluster (recommended: dxspots.com for reliability and speed)
CLUSTERS=[{"host":"dxspots.com","port":7300,"loginPrompt":"login:","call":"TE1ST","password":"","cluster":"DXSPOTS"}]Multiple clusters example:
CLUSTERS=[{"host":"dxspots.com","port":7300,"loginPrompt":"login:","call":"TE1ST","password":"","cluster":"DXSPOTS"},{"host":"dxc.nc7j.com","port":7300,"loginPrompt":"login:","call":"TE1ST","password":"","cluster":"NC7J"}]WAVELOG_URL=https://your-wavelog-instance.com/api/lookup
WAVELOG_KEY=your_api_key_here
# Maximum concurrent DXCC lookups (default: 2)
# Lower values reduce PHP-FPM memory usage but may slow spot processing
# Increase to 3-5 if your Wavelog server has plenty of RAM
MAX_CONCURRENT_DXCC=2# POTA Integration
POTA_ENABLED=true
POTA_POLLING_INTERVAL=120
# SOTA Integration
SOTA_ENABLED=true
SOTA_POLLING_INTERVAL=120
# Module Configuration
CLUSTER_ENABLED=true
ENRICHMENT_ENABLED=true
MODE_CLASSIFIER_ENABLED=true
ANALYTICS_ENABLED=false
# WebSocket (disable for some shared hosting)
WEBSOCKET_ENABLED=trueFILE_LOGGING_ENABLED=true
LOG_RETENTION_DAYS=3DXClusterAPI features a modular architecture where all major components can be enabled or disabled via .env configuration:
- Default:
true(enabled) - Purpose: Legacy REST API endpoints for backward compatibility
- Location:
modules/apiv1/ - Endpoints:
/spots,/spots/:band,/spots/source/:source,/spot/:qrg - Features:
- Direct JSON responses (no wrapper)
- No authentication
- Simple parameter-based filtering
Example:
API_V1_ENABLED=true- Default:
true(enabled) - Purpose: Modern REST API with advanced filtering and optional authentication
- Location:
modules/apiv2/ - Base Path:
/api/v2 - Features:
- Standardized JSON responses with metadata
- Optional API key authentication
- Advanced filtering (band, continent, frequency, activity types)
- Pagination support
Example:
API_V2_ENABLED=true
API_V2_KEY= # Leave empty for open access
API_SPOT_LIMIT=200 # Maximum spots returned by API endpoints (default limit)- Spot Limit (
API_SPOT_LIMIT): Default maximum number of spots returned by API endpoints- Default:
200 - Applies to API v1
/spots(always enforced) - Applies to API v2
/spots(whenlimitparameter not specified) - Spots are sorted by timestamp (newest first)
- Recommended: 100-500 depending on client needs
- API v2 can override with
?limit=Nparameter (max 500)
- Default:
Example:
API_SPOT_LIMIT=200 # Default limit for both API v1 and v2- Enabled (
API_V1_ENABLED): Enable/disable legacy API v1 endpoints- Default:
true - Includes:
/spots,/spots/:band,/spots/source/:source,/spot/:qrg
- Default:
Example:
API_V1_ENABLED=true- Default:
true(enabled) - Purpose: Manages connections to one or more DX Cluster servers
- Configuration: Set cluster details in
CLUSTERSJSON array - Location:
modules/clusters/ - Features:
- Multi-cluster support with automatic reconnection
- Connection health monitoring
- Spot aggregation from all configured clusters
Example:
CLUSTER_ENABLED=true
CLUSTERS=[{"host":"dxspots.com","port":7300,"loginPrompt":"login:","call":"TE1ST","password":"","cluster":"DXSPOTS"}]- Default:
true(enabled) - Purpose: Extracts metadata from spot messages (POTA/SOTA/IOTA/WWFF references, contest detection)
- Location:
modules/enrichment/ - Benefits: Automatically adds structured data to spots for easier filtering and display
Example:
ENRICHMENT_ENABLED=true- Default:
true(enabled) - Purpose: Automatically detects operating mode (CW/Phone/Digi) and submode (USB/LSB/FT8/RTTY/etc.) for all spots
- Location:
modules/modeclassifier/ - Classification Priority:
- Program-specific modes (POTA/SOTA/WWFF/IOTA reported modes)
- Message parsing (RBN includes mode in message: "CW 25 dB", "FT8 -10 dB")
- Mode field (if explicitly provided by spot source)
- Frequency-based band plan analysis (fallback)
- Mode Categories:
cw,phone,digi - Submodes:
CW,LSB,USB,SSB,AM,FM,FT8,FT4,RTTY,PSK31,PSK63,JT65,JT9,Q65, and more - Confidence Scoring: Each classification includes a confidence level (0.3 to 1.0)
Example:
MODE_CLASSIFIER_ENABLED=trueOutput Fields:
{
"mode": "digi",
"submode": "FT8"
}- Default:
true(enabled) - Purpose: Tracks API usage statistics (client IPs, endpoints, request counts)
- Location:
modules/analytics/ - Data: Stored in
data/analytics.json, accessible via/analyticsendpoint
Example:
ANALYTICS_ENABLED=true- Default:
true(enabled) - Purpose: Polls POTA API for Parks on the Air spots
- Polling Interval: Configurable (default 120 seconds)
Example:
POTA_ENABLED=true
POTA_POLLING_INTERVAL=120- Default:
true(enabled) - Purpose: Polls SOTA API for Summits on the Air spots
- Polling Interval: Configurable (default 120 seconds)
Example:
SOTA_ENABLED=true
SOTA_POLLING_INTERVAL=120- Default:
false(disabled) - Purpose: Connects to Reverse Beacon Network for automated CW/RTTY/FT8 spot detection
- Connections: Configurable - enable CW/RTTY feed, FT8 feed, or both
- Filtering: Shows only one spot per continent per spotted callsign for cleaner data
- Spot Timeout: Configurable (default 5 minutes)
Key Features:
- Connects to
telnet.reversebeacon.neton ports 7000 (CW/RTTY) and 7001 (FT8) - Continental filtering: Keeps best spot from each continent (NA, SA, EU, AS, AF, OC, AN)
- Automatic spot aging: Removes spots older than configured timeout
- Signal strength awareness: Updates to stronger signals when available
- Selective feed control: Enable only the modes you want (CW/RTTY, FT8, or both)
Configuration Options:
RBN_ENABLED=false # Enable RBN module (default: off for privacy)
RBN_CALLSIGN=YOUR_CALLSIGN # Your amateur radio callsign (required)
RBN_SPOT_TIMEOUT=5 # Spot timeout in minutes
RBN_CW_RTTY_ENABLED=true # Enable CW/RTTY feed (port 7000)
RBN_FT8_ENABLED=false # Enable FT8 feed (port 7001)Feed Options:
- CW/RTTY Feed (
RBN_CW_RTTY_ENABLED): Morse code and RTTY digital mode spots (default: enabled) - FT8 Feed (
RBN_FT8_ENABLED): FT8 digital mode spots (default: disabled due to high volume) - You can enable both feeds, only one, or neither (module disabled)
Note: Requires a valid amateur radio callsign for RBN login. RBN spots appear with source: "rbn" in the API. FT8 feed generates very high spot volumes - enable only if needed.
- Default:
true(enabled) - Purpose: Serves the built-in live monitoring web interface at
/live - Location:
views/live/index.html - Features: Real-time spot display, WebSocket updates, JSON inspection, logs viewer
Password Protection:
Optionally protect the live page with HTTP Basic Authentication by setting LIVE_PAGE_PASSWORD:
Example:
LIVE_PAGE_ENABLED=true
LIVE_PAGE_PASSWORD=MySecretPassword123Note:
- When disabled, the
/liveendpoint will not be available - When
LIVE_PAGE_PASSWORDis empty, the live page is publicly accessible - When
LIVE_PAGE_PASSWORDis set, browsers will prompt for authentication (username is ignored, only password is checked)
- Default:
true(enabled) - Purpose: Saves spot cache and DXCC cache to disk and restores on startup
- Location:
modules/persistence/ - Cache File:
data/spots-cache.json(configurable viaPERSISTENCE_PATH)
Configuration:
PERSISTENCE_ENABLED=true # Enable cache persistence
PERSISTENCE_INTERVAL=60 # Save every 60 seconds
PERSISTENCE_PATH= # Optional custom path (default: data/spots-cache.json)Features:
- Automatic Saves: Spot cache and DXCC cache saved to disk every interval (default: 60 seconds)
- Startup Restoration: Both caches loaded on app start, filtered for expired entries
- Atomic Writes: Uses temp file + rename to prevent corruption
- Index Rebuilding: All indexes (band, frequency, source, callsign) rebuilt from cached spots
- DXCC Cache Persistence: Up to 20,000 callsign lookups restored (7-day TTL per callsign)
- Statistics: Tracks save count, file size, duration, cache age, and DXCC entries
Benefits:
- API starts with existing spots instead of empty cache
- Eliminates DXCC API calls on restart (saves thousands of callsign lookups to Wavelog)
- Survives restarts/deployments without losing spot history or DXCC data
- Reduces initial cluster connection load and PHP-FPM pressure
- Maintains spot continuity for monitoring applications
Performance:
- Cache file size: ~200-500KB for 200 spots + ~500KB-2MB for 20k callsign lookups
- Save duration: 10-100ms depending on disk speed and cache size
- Load duration: 20-200ms including index rebuild and DXCC restoration
- Minimal I/O impact with 60+ second intervals
Note: Expired spots (SPOT_MAX_AGE) and DXCC entries (7 days) are automatically filtered during load.
- Status: Always enabled (cannot be disabled)
- Purpose: Provides Prometheus metrics for monitoring and observability
- Location:
modules/metrics/ - Endpoint:
/metrics - Features:
- HTTP request latency tracking
- Spot cache metrics
- WebSocket connection count
- Cluster connection status by cluster name
- Spots by band distribution
- Standard Node.js metrics (CPU, memory, GC, event loop)
Metrics exported:
http_request_duration_seconds- Request latency histogramdxcluster_spots_total- Total cached spotsdxcluster_websocket_connections- Active WebSocket connectionsdxcluster_cluster_connections- Cluster status (labeled)dxcluster_spots_by_band- Spot distribution by band
Note: No configuration needed - metrics are always available at /metrics endpoint.
- Default:
false(disabled, enable for production if needed) - Purpose: Protects API from abuse and DDoS attacks
- Location:
modules/rate-limiter/ - Configuration Options:
RATE_LIMITER_ENABLED: Enable/disable rate limitingRATE_LIMITER_GENERAL_MAX: Max requests for general endpoints (default: 120)RATE_LIMITER_DATA_MAX: Max requests for data endpoints (default: 60)RATE_LIMITER_BAN_TIME: Ban duration when limit exceeded, in seconds (default: 60)
- Features:
- Configurable rate limits per IP address
- Separate limits for general vs data endpoints (for spot polling)
- Configurable ban time for rate limit violations
- Automatic exemption for
/healthand/metrics - Applied to both API v1 and v2
- Proxy-aware for deployments behind reverse proxies
Default Rate Limits:
- General endpoints: 120 req/min (2 req/sec) - configurable via
RATE_LIMITER_GENERAL_MAX - Data endpoints: 60 req/min (1 req/sec) - configurable via
RATE_LIMITER_DATA_MAX - Ban duration: 60 seconds (1 minute) - configurable via
RATE_LIMITER_BAN_TIME
Recommended Settings:
- Low traffic:
RATE_LIMITER_GENERAL_MAX=60,RATE_LIMITER_DATA_MAX=30,RATE_LIMITER_BAN_TIME=60 - Medium traffic:
RATE_LIMITER_GENERAL_MAX=120,RATE_LIMITER_DATA_MAX=60,RATE_LIMITER_BAN_TIME=60(default) - High traffic:
RATE_LIMITER_GENERAL_MAX=240,RATE_LIMITER_DATA_MAX=120,RATE_LIMITER_BAN_TIME=300 - Strict security:
RATE_LIMITER_GENERAL_MAX=60,RATE_LIMITER_DATA_MAX=30,RATE_LIMITER_BAN_TIME=600 - Health/metrics: Unlimited
Easiest for production deployments
- Create your
.envfile from.env.sample - Build and start:
docker-compose up -dThe application will be available at http://localhost:3000 (or your configured port).
Useful commands:
# View logs
docker-compose logs -f
# Restart
docker-compose restart
# Stop
docker-compose downBest for development or VPS deployments
- Create your
.envfile withMODE=native - Install dependencies:
npm install- Start the application:
npm startRunning as a service (systemd example):
Create /etc/systemd/system/dxcluster.service:
[Unit]
Description=DXClusterAPI Service
After=network.target
[Service]
Type=simple
User=dxcluster
WorkingDirectory=/opt/dxclusterapi
ExecStart=/usr/bin/node index.js
Restart=always
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.targetEnable and start:
sudo systemctl enable dxcluster
sudo systemctl start dxclusterFor shared hosting environments
- Upload files to your hosting account
- Create
.envfile withMODE=passengerandWEBSOCKET_ENABLED=true - Install dependencies:
npm install - Configure via hosting control panel (not .htaccess)
- Restart Passenger:
mkdir -p tmp && touch tmp/restart.txtImportant Notes:
- MyDevil.net and similar hosts: Do NOT use
.htaccessfiles in the application directory. Configure the Node.js app through your hosting control panel (e.g., DevilWEB). The entry point should beindex.jsand the app type should be set tonodejs. - Static file collision: The live page is served through Express at
/liveand is not affected by static file hosting. - Application structure: Ensure files are in
~/domains/DOMAIN/public_nodejs/directory - Static files: The live page is in
views/live/- served through Express only - Restart: Use
mkdir -p tmp && touch tmp/restart.txtor hosting panel restart command
Typical directory structure:
/home/username/
├── domains/
│ └── yourdomain.com/
│ └── public_nodejs/ # Application root
│ ├── index.js # Entry point
│ ├── app.js
│ ├── .env # MODE=passenger
│ ├── modules/
│ │ ├── dxcluster/
│ │ ├── pota/
│ │ └── sota/
│ ├── public/ # Static files
│ │ └── index.html
│ ├── logs/
│ └── tmp/ # For restart signal
All endpoints are prefixed with BASEURL from your configuration.
DXClusterAPI supports both legacy API v1 and modern API v2 endpoints. Both can be enabled or disabled independently via .env configuration:
# Enable/disable API versions
API_V1_ENABLED=true
API_V2_ENABLED=true
# Optional: API key for v2 authentication (leave empty for open access)
API_V2_KEY=When both are enabled, API v1 endpoints remain at their original paths for backward compatibility, while API v2 endpoints are available at /api/v2/*.
Status: Active when API_V1_ENABLED=true (default)
Authentication: None required
Response Format: Direct JSON array or object
GET /
GET /infoReturns API information and available endpoints. Both endpoints return identical information.
Note: On shared hosting where the root / is handled by the web server (serving index.html), use /info to access API information via JSON.
Example response:
{
"name": "DXClusterAPI",
"version": "2.0.0",
"description": "Real-time DX Cluster, POTA, and SOTA spot aggregation API",
"api": {
"v1": {
"enabled": true,
"endpoints": {
"spots": "/spots",
"spotsByBand": "/spots/:band",
"spotsBySource": "/spots/source/:source",
"spotByFrequency": "/spot/:qrg",
"stats": "/stats",
"health": "/health",
"live": "/live"
}
},
"v2": {
"enabled": true,
"requiresAuth": false,
"endpoints": {
"spots": "/api/v2/spots",
"spotByCallsign": "/api/v2/spots/:callsign",
"bands": "/api/v2/bands",
"sources": "/api/v2/sources",
"heatmap": "/api/v2/heatmap",
"info": "/api/v2/info"
}
}
},
"documentation": "https://github.com/int2001/DXClusterAPI"
}GET /liveInteractive web interface showing real-time spots. The page will automatically:
- Try WebSocket first for real-time streaming (if
WEBSOCKET_ENABLED=true) - Fall back to polling if WebSocket is unavailable (common on shared hosting)
- Display connection status and mode (WebSocket or Polling)
Access at http://yourserver.com/live.
GET /spotsReturns the latest cached spots with DXCC information, sorted by timestamp (newest first). The number of spots returned is limited by the API_SPOT_LIMIT environment variable (default: 200).
Configuration:
- Set
API_SPOT_LIMITin.envto change the limit (default: 200) - Spots are automatically sorted by timestamp, newest first
- This ensures consistent response size and optimal performance
Example response:
[
{
"spotter": "W1ABC",
"spotted": "DL2XYZ",
"frequency": 14074,
"message": "FT8 loud and clear",
"when": "2025-11-09T12:34:56.789Z",
"source": "cluster",
"band": "20m",
"mode": "digi",
"submode": "FT8",
"dxcc_spotter": {
"cont": "NA",
"entity": "United States",
"flag": "🇺🇸",
"dxcc_id": "291",
"lotw_user": true,
"lat": "37.09",
"lng": "-95.71",
"cqz": "4"
},
"dxcc_spotted": {
"cont": "EU",
"entity": "Fed. Rep. Of Germany",
"flag": "🇩🇪",
"dxcc_id": "230",
"lotw_user": true,
"lat": "51.3",
"lng": "10.4",
"cqz": "14",
"sota_ref": "",
"pota_ref": "",
"iota_ref": "",
"wwff_ref": "",
"isContest": false
}
}
]Note: For more advanced filtering and pagination, use API v2 endpoints which support query parameters like limit, offset, band, continent, maxAge, etc.
Spot Metadata Enrichment:
All spots are automatically enriched with metadata extracted from the message field (when ENRICHMENT_ENABLED=true):
- SOTA References: Detected format
XX/YY-###(e.g., "G/LD-001", "W4G/NG-001") - POTA References: Detected format
XX-####(e.g., "K-1234", "US-4306") - IOTA References: Detected format
XX-###(e.g., "EU-005", "NA-001") - WWFF References: Detected format
XXFF-####(e.g., "KFF-6731", "GIFF-0001") - Contest Detection: Automatically flags contest-related spots with
isContest: trueand identifies the contest type incontestName- Uses word-boundary matching to avoid false positives
- Filters out conversational messages (TU, TNX, 73, GL, etc.)
- Excludes generic "CQ DX " patterns
- Detects typical contest exchanges (599/5NN + serial number patterns)
Enrichment Configuration:
- Set
ENRICHMENT_ENABLED=truein.envto enable automatic enrichment (default: enabled) - Set
ENRICHMENT_ENABLED=falseto disable if you don't need park references or contest detection - Enrichment module located in
modules/enrichment/
Smart Enrichment:
- Enrichment is performed ONLY if the spot doesn't already contain these fields
- If a spot already has enrichment fields they are preserved
- This prevents double-enrichment when chaining multiple APIs
- Module-specific data (POTA/SOTA) takes priority over message parsing
This enrichment is performed on ALL spots from all sources (DX Clusters, POTA, SOTA) when enabled.
GET /spots/:bandExample:
GET /spots/20mGET /spots/source/:sourceSources: cluster, pota, sota, rbn
Example:
GET /spots/source/potaPOTA spot example:
{
"spotter": "K1ABC",
"spotted": "W2XYZ",
"frequency": 14310,
"message": "SSB POTA @ K-1234 My State Park (My State)",
"when": "2025-11-09T14:30:00.000Z",
"source": "pota",
"band": "20m",
"mode": "phone",
"submode": "USB",
"dxcc_spotter": {},
"dxcc_spotted": {
"pota_ref": "K-1234",
"pota_mode": "SSB",
"sota_ref": "",
"iota_ref": "",
"wwff_ref": "",
"isContest": false
}
}SOTA spot example:
{
"spotter": "G1ABC",
"spotted": "GM2XYZ",
"frequency": 7032,
"message": "CW SOTA @ GM/SS-001 Ben Nevis (Scotland)",
"when": "2025-11-09T15:45:00.000Z",
"source": "sota",
"band": "40m",
"mode": "cw",
"submode": "CW",
"dxcc_spotter": {},
"dxcc_spotted": {
"sota_ref": "GM/SS-001",
"sota_mode": "CW",
"pota_ref": "",
"iota_ref": "",
"wwff_ref": "",
"isContest": false
}
}Contest spot example:
{
"spotter": "W1AW",
"spotted": "LZ2XYZ",
"frequency": 14025,
"message": "CQ WW DX Contest 599 023",
"when": "2025-11-09T16:00:00.000Z",
"source": "cluster",
"band": "20m",
"mode": "cw",
"submode": "CW",
"dxcc_spotter": {},
"dxcc_spotted": {
"entity": "Bulgaria",
"flag": "🇧🇬",
"sota_ref": "",
"pota_ref": "",
"iota_ref": "",
"wwff_ref": "",
"isContest": true,
"contestName": "CQ WW DX CONTEST"
}
}IOTA spot example:
{
"spotter": "G4XYZ",
"spotted": "SV9/DL1ABC",
"frequency": 14260,
"message": "EU-001 Crete Island",
"when": "2025-11-09T17:00:00.000Z",
"source": "cluster",
"band": "20m",
"mode": "phone",
"submode": "USB",
"dxcc_spotter": {},
"dxcc_spotted": {
"entity": "Crete",
"flag": "🇬🇷",
"sota_ref": "",
"pota_ref": "",
"iota_ref": "EU-015",
"wwff_ref": "",
"isContest": false
}
}GET /spot/:qrgExample:
GET /spot/14074Returns the most recent spot on the specified frequency (in kHz).
GET /statsReturns cache statistics with per-source breakdown.
Example response:
{
"entries": 150,
"cluster": 100,
"pota": 30,
"sota": 20,
"sources": {
"NC7J": 45,
"DXSPOTS": 55,
"pota": 30,
"sota": 20
},
"freshest": "2025-11-09T12:34:56.789Z",
"oldest": "2025-11-09T10:15:23.456Z"
}The sources object shows how many spots came from each configured cluster (e.g., NC7J, DXSPOTS) and external sources (pota, sota).
GET /healthReturns application health status for monitoring and load balancers.
Example response:
{
"status": "ok",
"timestamp": "2025-11-09T12:34:56.789Z",
"uptime": 86400,
"mode": "docker",
"memory": {
"used": 45,
"total": 64
},
"cache": {
"spots": 150,
"maxcache": 200
},
"modules": {
"cluster": true,
"pota": true,
"sota": true,
"enrichment": true,
"websocket": true,
"apiv1": {
"enabled": true
},
"apiv2": {
"enabled": true,
"requiresAuth": false
}
}
}GET /analyticsReturns detailed information about API clients and usage patterns. Tracks who is accessing your API endpoints.
Configuration:
- Enable/disable via
ANALYTICS_ENABLED=true/falsein.env - Data stored in
data/analytics.json - Automatically saves every 5 minutes and on shutdown
- Module located in
modules/analytics/
Example response:
{
"summary": {
"enabled": true,
"totalClients": 5,
"totalRequests": 12453,
"dataFile": "/path/to/data/analytics.json"
},
"endpointStats": {
"/spots": {
"totalRequests": 8500,
"uniqueClients": 4
},
"/spots/20m": {
"totalRequests": 2100,
"uniqueClients": 3
}
},
"clients": [
{
"ip": "192.168.1.100",
"userAgent": "Mozilla/5.0...",
"firstSeen": "2025-11-01T10:00:00.000Z",
"lastSeen": "2025-11-09T12:34:56.000Z",
"totalRequests": 5234,
"endpoints": {
"/spots": {
"count": 4000,
"methods": {
"GET": 4000
},
"lastAccess": "2025-11-09T12:34:56.000Z"
},
"/spots/20m": {
"count": 1234,
"methods": {
"GET": 1234
},
"lastAccess": "2025-11-09T12:30:00.000Z"
}
},
"referers": [
"https://example.com/dxspots"
]
}
]
}Tracked Information:
- IP address (with proxy support via X-Forwarded-For)
- User agent (browser/application identifier)
- First and last seen timestamps
- Total request count per client
- Per-endpoint statistics (count, HTTP methods, last access)
- Referrers (where traffic originates from)
Excluded from Tracking:
/healthendpoint/liveendpoint/analyticsendpoint itself
Status: Active when API_V2_ENABLED=true (default)
Base Path: /api/v2
Authentication: Optional API key via X-API-Key header
Client Identification: Optional X-Client-ID header for analytics tracking
Parameters: URL query parameters (not JSON body)
Response Format: Standardized JSON wrapper with metadata
API v2 provides a modern REST interface with:
- Standardized response format with status, version, timestamp
- Optional authentication via API key
- Optional client identification for tracking (e.g., Wavelog instance ID)
- Advanced filtering via URL query parameters (band, continent, source, age, frequency range, activity types)
- Pagination support (limit, offset)
- Consistent error handling
Set API_V2_KEY in .env to require authentication:
# No authentication (open access)
API_V2_KEY=
# Require API key
API_V2_KEY=your-secret-api-key-hereWhen authentication is enabled, include the API key in the X-API-Key header:
curl -H "X-API-Key: your-secret-api-key-here" http://localhost:3000/api/v2/spotsOptional Client Identification:
Both API v1 and v2 support the X-Client-ID header for identifying API clients in analytics. This is useful for tracking specific Wavelog instances or other API consumers:
curl -H "X-Client-ID: wavelog-ok1abc" http://localhost:3000/api/v2/spotsThe client ID will be tracked in analytics data alongside IP addresses and user agents, allowing you to:
- Identify which Wavelog instances are using your API
- Track usage patterns by specific clients
- Monitor API consumption per client
Unauthenticated request (when API key is required):
{
"status": "error",
"version": "2.0.0",
"timestamp": "2025-11-09T12:34:56.789Z",
"error": {
"code": "AUTH_REQUIRED",
"message": "X-API-Key header is required"
}
}All API v2 endpoints return a standardized JSON wrapper:
{
"status": "success",
"version": "2.0.0",
"timestamp": "2025-11-09T12:34:56.789Z",
"data": [...],
"meta": {
"total": 150,
"returned": 50,
"filters": {...}
}
}Fields:
status:"success"or"error"version: API version (matches app version)timestamp: ISO 8601 timestamp of the responsedata: Response payload (array or object)meta: Metadata about the response (counts, filters, pagination)error: Error details (only present whenstatus: "error")
GET /api/v2/spotsReturns all spots matching the specified filters. All filters are passed as URL query parameters.
Query Parameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
band |
string | Filter by band (e.g., "20m", "40m") | ?band=20m |
continent |
string | Filter by continent code (NA, EU, AS, etc.) | ?continent=EU |
source |
string | Filter by source (cluster, pota, sota, or cluster name) | ?source=pota |
maxAge |
number | Maximum age in seconds | ?maxAge=300 |
minFreq |
number | Minimum frequency in kHz | ?minFreq=14000 |
maxFreq |
number | Maximum frequency in kHz | ?maxFreq=14350 |
contest |
boolean | Filter contest spots (true/false) | ?contest=true |
pota |
boolean | Filter POTA spots (true/false) | ?pota=true |
sota |
boolean | Filter SOTA spots (true/false) | ?sota=true |
iota |
boolean | Filter IOTA spots (true/false) | ?iota=true |
wwff |
boolean | Filter WWFF spots (true/false) | ?wwff=true |
limit |
number | Maximum spots to return (default: API_SPOT_LIMIT from config, max: 500) |
?limit=50 |
offset |
number | Number of spots to skip (for pagination) | ?offset=100 |
Default Behavior:
- If
limitis not specified: Returns up toAPI_SPOT_LIMITspots (default: 200) - If
limitis specified: Returns up to the specified amount (max: 500) - Use
limit=500to get maximum spots per request - Spots are sorted by timestamp (newest first)
Examples:
# Get all 20m spots from the last 5 minutes
curl http://localhost:3000/api/v2/spots?band=20m&maxAge=300
# Get POTA spots only
curl http://localhost:3000/api/v2/spots?pota=true
# Get European stations on 40m CW
curl http://localhost:3000/api/v2/spots?continent=EU&minFreq=7000&maxFreq=7040
# Get contest spots only, limit to 20
curl http://localhost:3000/api/v2/spots?contest=true&limit=20
# Pagination: Get next 50 spots
curl http://localhost:3000/api/v2/spots?limit=50&offset=50Example Response:
{
"status": "success",
"version": "2.0.0",
"timestamp": "2025-11-09T12:34:56.789Z",
"data": [
{
"spotter": "W1ABC",
"spotted": "DL2XYZ",
"frequency": 14074,
"message": "FT8 loud and clear",
"when": "2025-11-09T12:34:00.000Z",
"source": "NC7J",
"band": "20m",
"mode": "digi",
"submode": "FT8",
"dxcc_spotter": {
"cont": "NA",
"entity": "United States",
"flag": "🇺🇸",
"dxcc_id": "291",
"lotw_user": true,
"lat": "37.09",
"lng": "-95.71",
"cqz": "4"
},
"dxcc_spotted": {
"cont": "EU",
"entity": "Fed. Rep. Of Germany",
"flag": "🇩🇪",
"dxcc_id": "230",
"lotw_user": true,
"lat": "51.3",
"lng": "10.4",
"cqz": "14",
"sota_ref": "",
"pota_ref": "",
"iota_ref": "",
"wwff_ref": "",
"isContest": false
}
}
],
"meta": {
"total": 150,
"returned": 1,
"filters": {
"band": "20m",
"maxAge": 300
}
}
}GET /api/v2/spots/:callsignReturns all spots where the specified callsign appears as either spotter or spotted station.
Example:
curl http://localhost:3000/api/v2/spots/W1ABCGET /api/v2/spot/:qrgReturns the latest spot at the specified frequency (in kHz).
Example Response:
{
"status": "success",
"version": "2.0.0",
"timestamp": "2025-11-09T12:34:56.789Z",
"data": [
{
"spotter": "W1ABC",
"spotted": "DL2XYZ",
"frequency": 14074,
"message": "FT8",
"when": "2025-11-09T12:30:00.000Z",
"source": "NC7J",
"band": "20m",
"mode": "digi",
"submode": "FT8"
},
{
"spotter": "K2DEF",
"spotted": "W1ABC",
"frequency": 7074,
"message": "Working POTA",
"when": "2025-11-09T12:25:00.000Z",
"source": "pota",
"band": "40m",
"mode": "digi",
"submode": "FT8"
}
],
"meta": {
"callsign": "W1ABC",
"total": 2,
"returned": 2
}
}Example Response:
{
"status": "success",
"version": "2.0.0",
"timestamp": "2025-11-09T12:34:56.789Z",
"data": {
"spotter": "W1ABC",
"spotted": "DL2XYZ",
"frequency": 14074,
"message": "FT8 loud and clear",
"when": "2025-11-09T12:34:00.000Z",
"source": "NC7J",
"band": "20m",
"mode": "digi",
"submode": "FT8",
"dxcc_spotter": {
"cont": "NA",
"entity": "United States",
"flag": "🇺🇸"
},
"dxcc_spotted": {
"cont": "EU",
"entity": "Fed. Rep. Of Germany",
"flag": "🇩🇪"
}
},
"meta": {
"frequency": 14074
}
}Example (spot not found):
{
"status": "error",
"version": "2.0.0",
"timestamp": "2025-11-09T12:34:56.789Z",
"error": "No spot found at this frequency",
"data": null,
"meta": {
"frequency": 21074
}
}The /api/v2/spots endpoint supports filtering by operating mode and submode using query parameters.
Mode Filter (mode):
- Supports comma-separated values:
cw,phone,digi - Returns spots matching ANY of the specified modes (OR logic)
Submode Filter (submode):
- Supports comma-separated values:
FT8,FT4,CW,USB,LSB,SSB,AM,FM,RTTY,PSK31,PSK63,JT65,JT9,Q65, etc. - Returns spots matching ANY of the specified submodes (OR logic)
Examples:
# Get CW spots only
curl http://localhost:3000/api/v2/spots?mode=cw
# Get phone and CW spots (OR logic - returns all CW OR phone spots)
curl http://localhost:3000/api/v2/spots?mode=phone,cw
# Get all digital spots
curl http://localhost:3000/api/v2/spots?mode=digi
# Get FT8 spots only
curl http://localhost:3000/api/v2/spots?submode=FT8
# Get FT8 and FT4 spots (OR logic)
curl http://localhost:3000/api/v2/spots?submode=FT8,FT4
# Get all PSK spots
curl http://localhost:3000/api/v2/spots?submode=PSK31,PSK63
# Combine with other filters
curl http://localhost:3000/api/v2/spots?mode=digi&band=20m&maxAge=300Example Response:
{
"status": "success",
"version": "2.0.0",
"timestamp": "2025-11-09T12:34:56.789Z",
"data": [
{
"spotter": "RBN",
"spotted": "W1ABC",
"frequency": 14025,
"message": "CW 25 dB",
"when": "2025-11-09T12:30:00.000Z",
"source": "rbn",
"band": "20m",
"mode": "cw",
"submode": "CW"
},
{
"spotter": "DL1ABC",
"spotted": "W1XYZ",
"frequency": 7040,
"message": "SSB",
"when": "2025-11-09T12:28:00.000Z",
"source": "NC7J",
"band": "40m",
"mode": "phone",
"submode": "LSB"
}
],
"meta": {
"total": 2,
"limit": 100,
"offset": 0,
"returned": 2,
"filters": {
"mode": "cw,phone"
}
}
}GET /api/v2/bandsReturns a list of all bands currently present in the spot cache with spot counts.
Example Response:
{
"status": "success",
"version": "2.0.0",
"timestamp": "2025-11-09T12:34:56.789Z",
"data": [
{
"band": "20m",
"count": 45
},
{
"band": "40m",
"count": 32
},
{
"band": "80m",
"count": 18
}
],
"meta": {
"totalBands": 3,
"totalSpots": 95
}
}GET /api/v2/sourcesReturns a list of all sources currently present in the spot cache with spot counts.
Example Response:
{
"status": "success",
"version": "2.0.0",
"timestamp": "2025-11-09T12:34:56.789Z",
"data": [
{
"source": "NC7J",
"type": "cluster",
"count": 45
},
{
"source": "DXSPOTS",
"type": "cluster",
"count": 38
},
{
"source": "pota",
"type": "external",
"count": 30
},
{
"source": "sota",
"type": "external",
"count": 22
}
],
"meta": {
"totalSources": 4,
"totalSpots": 135
}
}GET /api/v2/heatmap
GET /api/v2/heatmap?continent=EUReturns band activity heatmap showing number of spots organized by DE continent (spotter), band, and DX continent (spotted station). Data is cached for 15 minutes to optimize performance.
Query Parameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
continent |
string | Filter by DE continent (spotter's continent) | ?continent=EU |
Example Response:
{
"status": "success",
"version": "2.0.0",
"timestamp": "2025-11-10T15:30:00.000Z",
"data": {
"continents": ["EU", "NA", "SA", "AS", "AF", "OC"],
"bands": ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m", "4m", "2m", "70cm"],
"data": {
"EU": {
"20m": {
"EU": 45,
"NA": 23,
"AS": 12,
"AF": 8,
"SA": 5,
"OC": 3
},
"40m": {
"EU": 34,
"NA": 18,
"AS": 7
}
},
"NA": {
"20m": {
"NA": 38,
"EU": 29,
"SA": 15
}
}
},
"generatedAt": "2025-11-10T15:30:00.000Z",
"totalSpots": 1234,
"cacheExpiresIn": 900
},
"meta": {
"cached": true,
"cacheAgeSeconds": 45,
"cacheTTLSeconds": 900
}
}Use Cases:
- Generate band activity heatmaps like your attached image
- Visualize propagation patterns between continents
- Identify optimal bands for specific DE→DX continent paths
- Monitor real-time band conditions by geographic region
Example: Get only European spotter activity:
curl "https://your-api.com/api/v2/heatmap?continent=EU"GET /api/v2/infoReturns information about the API v2 capabilities, available filters, and configuration.
Example Response:
{
"status": "success",
"version": "2.0.0",
"timestamp": "2025-11-09T12:34:56.789Z",
"data": {
"name": "DXClusterAPI v2",
"version": "2.0.0",
"description": "Modern REST API for DX Cluster spots with advanced filtering",
"authentication": {
"required": false,
"method": "X-API-Key header"
},
"endpoints": {
"spots": "/api/v2/spots",
"spotsByCallsign": "/api/v2/spots/:callsign",
"spotByFrequency": "/api/v2/spot/:qrg",
"bands": "/api/v2/bands",
"sources": "/api/v2/sources",
"info": "/api/v2/info"
},
"filters": {
"band": "Filter by band (e.g., 20m, 40m)",
"continent": "Filter by continent code (NA, EU, AS, AF, OC, SA, AN)",
"source": "Filter by source name",
"maxAge": "Maximum age in seconds",
"minFreq": "Minimum frequency in kHz",
"maxFreq": "Maximum frequency in kHz",
"contest": "Filter contest spots (true/false)",
"pota": "Filter POTA spots (true/false)",
"sota": "Filter SOTA spots (true/false)",
"iota": "Filter IOTA spots (true/false)",
"wwff": "Filter WWFF spots (true/false)",
"limit": "Maximum spots to return (default: 100, max: 500)",
"offset": "Number of spots to skip (pagination)"
},
"features": [
"Standardized JSON responses",
"Optional API key authentication",
"Advanced multi-parameter filtering",
"Pagination support",
"Continent-based filtering",
"Activity type filtering (POTA/SOTA/IOTA/WWFF)",
"Contest detection",
"Frequency range filtering",
"Time-based filtering (maxAge)"
]
}
}All errors return a standardized error response:
{
"status": "error",
"version": "2.0.0",
"timestamp": "2025-11-09T12:34:56.789Z",
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message"
}
}Common Error Codes:
AUTH_REQUIRED: API key is required but not providedINVALID_API_KEY: Provided API key is incorrectINVALID_PARAMETER: Query parameter has invalid valueNOT_FOUND: Requested resource not found
Complex Filtering:
# European POTA activations on 20m from the last 10 minutes
curl "http://localhost:3000/api/v2/spots?continent=EU&pota=true&band=20m&maxAge=600"
# Contest spots on CW bands (7000-7040 kHz)
curl "http://localhost:3000/api/v2/spots?contest=true&minFreq=7000&maxFreq=7040"
# SOTA spots on VHF (144 MHz)
curl "http://localhost:3000/api/v2/spots?sota=true&minFreq=144000&maxFreq=148000"With Authentication:
# Set API key in header
export API_KEY="your-secret-api-key-here"
curl -H "X-API-Key: $API_KEY" "http://localhost:3000/api/v2/spots?band=20m"JavaScript (fetch API):
// Without authentication - using URL query parameters
const response = await fetch('http://localhost:3000/api/v2/spots?band=20m&maxAge=300');
const data = await response.json();
if (data.status === 'success') {
console.log(`Found ${data.meta.returned} spots on 20m`);
data.data.forEach(spot => {
console.log(`${spot.spotted} on ${spot.frequency} kHz`);
});
}
// With authentication
const authResponse = await fetch('http://localhost:3000/api/v2/spots?pota=true', {
headers: {
'X-API-Key': 'your-secret-api-key-here'
}
});Note: API v2 uses URL query parameters (not JSON body). All filters are passed as query strings in the URL.
GET /infoAccess the built-in web interface for viewing spots.
Connect to the same host/port for real-time spot streaming:
const ws = new WebSocket('ws://localhost:3000');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'spot') {
console.log('New spot:', data.data);
}
};Edit your .env file:
# Enable/Disable API versions
API_V1_ENABLED=true
API_V2_ENABLED=true
API_V2_KEY= # Leave empty for open access
# Enable POTA
POTA_ENABLED=true
POTA_POLLING_INTERVAL=120
# Disable SOTA
SOTA_ENABLED=false
# Disable WebSocket (for Passenger/shared hosting)
WEBSOCKET_ENABLED=false
# Other modules
CLUSTER_ENABLED=true
ENRICHMENT_ENABLED=true
MODE_CLASSIFIER_ENABLED=true
ANALYTICS_ENABLED=true
LIVE_PAGE_ENABLED=trueFuture modules (WWFF, etc.) can be added by:
- Creating a new module directory (e.g.,
modules/wwff/) - Adding configuration to
.env.sample - Initializing in
app.jssimilar to POTA/SOTA
npm run devDXClusterAPI/
├── index.js # Entry point / launcher
├── app.js # Main application
├── .env # Configuration (gitignored)
├── .env.sample # Configuration template
├── package.json # Dependencies
├── docker-compose.yaml # Docker configuration
├── Dockerfile # Docker image
├── .htaccess.example # Passenger configuration example
├── lib/ # Shared libraries
│ ├── dxcluster.js # DX Cluster connection class (used by clusters & RBN)
│ └── utils.js # Shared utility functions
├── modules/
│ ├── apiv1/ # API v1 module (legacy)
│ │ └── index.js
│ ├── apiv2/ # API v2 module (modern)
│ │ └── index.js
│ ├── clusters/ # Cluster connections module
│ │ └── index.js
│ ├── enrichment/ # Spot enrichment module
│ │ └── index.js
│ ├── analytics/ # API analytics module
│ │ └── index.js
│ ├── metrics/ # Prometheus metrics module
│ │ └── index.js
│ ├── rate-limiter/ # Rate limiting module
│ │ └── index.js
│ ├── pota/ # POTA module
│ │ └── index.js
│ ├── sota/ # SOTA module
│ │ └── index.js
│ ├── rbn/ # RBN module
│ │ └── index.js
│ └── modeclassifier/ # Mode classification module
│ └── index.js
├── public/
│ ├── info/
│ │ └── index.html # Info web interface
│ ├── index.html # Redirect to wavelog.org
│ └── robots.txt # Search engine exclusion
├── data/ # Data files (analytics, etc.)
└── logs/ # Log files (auto-created)
Edit your .env file and add to the CLUSTERS array:
CLUSTERS=[
{"host":"dxspots.com","port":7300,"loginPrompt":"login:","call":"TE1ST","password":"","cluster":"DXSPOTS"},
{"host":"dxc.nc7j.com","port":7300,"loginPrompt":"login:","call":"TE1ST","password":"","cluster":"NC7J"}
]Popular DX Clusters:
dxspots.com:7300- Fast, reliable, worldwide coverage (recommended)dxc.nc7j.com:7300- Well-established, North American focusdxfun.com:8000- Alternative cluster option
WebSocket protocol upgrades are often blocked by reverse proxies on shared hosting. The live page automatically detects this and falls back to polling mode (fetching /spots every 2 seconds).
Options:
- Recommended: Leave
WEBSOCKET_ENABLED=true- the live page will auto-detect and use polling - Alternative: Set
WEBSOCKET_ENABLED=falseto disable WebSocket server initialization entirely
Both approaches work on shared hosting. The polling fallback provides a seamless user experience.
- Verify
WAVELOG_URLandWAVELOG_KEYare correct - Check network connectivity to the Wavelog server
- Review logs in
logs/directory
- Check
.envfile exists and is properly formatted - Verify all required environment variables are set
- Check logs:
tail -f logs/app-YYYYMMDD.log - Ensure port is not already in use
# Check logs
docker-compose logs
# Rebuild container
docker-compose down
docker-compose build --no-cache
docker-compose up -d- Reduce
MAXCACHEin.env - Increase polling intervals for POTA/SOTA
- Limit number of connected DX clusters
- Check
SPOT_MAX_AGEsetting (default 120 minutes)
The API includes built-in rate limiting to protect against abuse and DDoS attacks. Rate limiting is disabled by default and can be enabled via environment variables.
Configuration:
RATE_LIMITER_ENABLED=true # Enable rate limiting (default: false)
RATE_LIMITER_GENERAL_MAX=120 # Requests/minute for general endpoints (default: 120)
RATE_LIMITER_DATA_MAX=60 # Requests/minute for data endpoints (default: 60)
RATE_LIMITER_BAN_TIME=60 # Ban time in seconds when limit exceeded (default: 60)Default limits (configurable):
- General endpoints: 120 requests/minute per IP
- Spot data endpoints: 60 requests/minute per IP
- Exempt endpoints:
/healthand/metrics(for monitoring)
Rate limits are applied automatically to:
- API v1:
/spots,/spots/:band,/spots/source/:source,/spot/:qrg - API v2:
/api/v2/spots,/api/v2/spots/:callsign
When rate limit is exceeded, clients receive HTTP 429:
{
"error": "Too many requests. Try again after 60 seconds"
}The default 60 requests/minute limit for data endpoints accommodates typical client polling intervals (once per second or longer).
Monitor your API with Prometheus metrics at /metrics:
GET /metricsAvailable metrics:
http_request_duration_seconds- HTTP request latency histogramdxcluster_spots_total- Total number of cached spotsdxcluster_websocket_connections- Active WebSocket connectionsdxcluster_cluster_connections- DX cluster connection status by clusterdxcluster_spots_by_band- Spot count per band (20m, 40m, etc.)- Plus standard Node.js metrics (CPU, memory, event loop, GC)
Prometheus configuration example:
scrape_configs:
- job_name: 'dxcluster-api'
scrape_interval: 30s
static_configs:
- targets: ['your-server.com:3000']
metrics_path: '/metrics'Grafana dashboard tips:
- Track request latency with
http_request_duration_seconds - Monitor spot cache size with
dxcluster_spots_total - Alert on cluster disconnections with
dxcluster_cluster_connections - Visualize band activity with
dxcluster_spots_by_band
Protect API v2 endpoints with API key authentication:
# In .env file
API_V2_KEY=your-secret-key-hereClients must include the key in requests:
GET /api/v2/spots
X-API-Key: your-secret-key-hereWithout a valid key, API v2 returns 401 Unauthorized. API v1 remains open for backward compatibility.
If you experience API rate limiting from POTA or SOTA:
- Increase
POTA_POLLING_INTERVALandSOTA_POLLING_INTERVALin.env - Minimum recommended: 120 seconds (2 minutes)
- POTA API has usage limits - respect them by not polling too frequently
The application features an aggressive DXCC caching system optimized to minimize RAM usage on shared hosting:
Caching Strategy:
- Callsign Normalization: Strips
/P,/M,/QRPand other portable suffixes to increase cache hits - Large Cache Size: 20,000 callsigns (default)
- Long TTL: 7-day callsign cache with automatic cleanup
- Concurrency Limiting: Configurable concurrent DXCC lookups (default: 2, set via
MAX_CONCURRENT_DXCC) - LRU Eviction: Least Recently Used entries removed when cache is full
- Failed Lookup Cache: 5-minute cache for bad callsigns to prevent repeated failed lookups
- Optimized Wavelog API: Uses
lookup_v2()endpoint for minimal PHP overhead
Cache Hit Optimization:
W1ABCandW1ABC/P→ Same cache entry (normalized toW1ABC)- Full callsign lookup caches accurate DXCC + LoTW data for 7 days
- Duplicate lookups return instantly from cache without PHP calls
Expected Results:
- Cache Hit Rate: 95%+ after warmup period
- PHP Requests: Reduced by 95%+ compared to no caching
- RAM Usage: 600MB total (was 3.3GB) on FreeBSD hosting with 1GB limit
- PHP-FPM Processes: Reduced from 24 to 2-3 processes
Configuration:
- Set
MAX_CONCURRENT_DXCCin.envto control concurrent PHP requests (default: 2) - Lower values (1-2) reduce PHP-FPM memory usage but slow spot processing
- Higher values (3-5) speed up processing but require more PHP-FPM workers
- See
DXCC_OPTIMIZATION.mdfor technical details
Memory Optimization Features:
- Bounded spot array with automatic LRU eviction
- WebSocket ping/pong for detecting dead clients (30-second heartbeat)
- All module caches bounded with size limits (POTA: 500, SOTA: 500, RBN: 2000, Analytics: 1000 clients)
- Map-based caching for O(1) lookups instead of O(n) array scans
- Optimized spot removal using binary search (O(n log n) instead of O(n²))
Hosting Requirements:
- Minimum RAM: 1.5GB recommended (was 1GB)
- Node.js: v18+ recommended
- External Service: Wavelog PHP-FPM for DXCC lookups (can consume significant RAM if cache isn't working)
Troubleshooting High RAM Usage:
If you're experiencing high RAM usage (>1GB):
-
Check if PHP-FPM is the culprit (not Node.js):
top -o res # FreeBSD ps aux | grep php-fpm # Linux
If you see many PHP-FPM processes (>10) each using 100MB+, the issue is likely DXCC lookups.
-
Verify DXCC cache is working:
curl http://yourserver.com/health | json_ppCheck
cache.dxccCachevalue. It should grow over time. -
Check live page memory details:
- Open live page (
/live) - Click the "🧠 System Info" button
- Review cache statistics and memory usage
- Open live page (
-
Reduce cache size if needed (edit
app.js):const DXCC_CACHE_MAX_SIZE = 10000; // Reduce from 20000
-
Increase PHP-FPM cache on external server (if you control Wavelog):
- Configure PHP OPcache
- Increase APCu cache size
- Consider using Redis for DXCC caching in Wavelog
See DXCC_OPTIMIZATION.md for comprehensive technical documentation.
- Increase cache size: Set
MAXCACHE=500or higher for busy servers - Use WebSocket: Enables push instead of pull for connected clients
- Adjust polling intervals: Balance freshness vs server load
- Monitor memory: Check
/healthendpoint regularly - Enable file logging: Set
FILE_LOGGING_ENABLED=truefor debugging - Restrict browser access: Edit CORS settings in
app.jsif needed to limit which domains can access your API
- Application logs HTTP requests in access.log style to console (or log files if
FILE_LOGGING_ENABLED=true) - Use PM2 for production logging:
pm2 start index.js --name dxcluster-api - Log files are automatically rotated based on
LOG_RETENTION_DAYSsetting
# Recommended .env settings for production
MODE=docker
MAXCACHE=500
POTA_POLLING_INTERVAL=120
SOTA_POLLING_INTERVAL=120
ENRICHMENT_ENABLED=true
MODE_CLASSIFIER_ENABLED=true
WEBSOCKET_ENABLED=true
FILE_LOGGING_ENABLED=true
LOG_RETENTION_DAYS=7MIT
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
For issues and questions:
- GitHub Issues: https://github.com/int2001/DXClusterAPI/issues
- Documentation: See this README
- @int2001 - Joerg (DJ7NT) - Original project creator
- @DB4SCW - Stefan (DB4SCW) - Contributor
- @winnieXY - Patrick Winnertz - Contributor
- @szporwolik - Szymon Porwolik - Version 2.0.0 refactor and enhancements