Technical Details

Table of contents

  1. Architecture Overview
    1. Key Components
  2. Data Providers
    1. ESPN Provider
    2. ESPN Athlete Provider
    3. TheSportsDB Provider
    4. HockeyTech Provider
    5. MLBStats Provider
    6. FlagCDN Provider
  3. Provider System
    1. Automatic Provider Discovery
  4. Image Generation
    1. Canvas Rendering
    2. Visual Styles
      1. Thumbnail & Cover Styles
      2. Logo Styles
    3. Logo Selection
    4. Aspect Ratio Preservation
  5. Color Extraction
    1. Process
    2. Algorithm
    3. Fallback
  6. Team Resolution & Fallback System
    1. Resolution Chain
    2. Custom Teams (No Provider Lookup)
    3. Unsupported League Fallback (Provider Interface)
    4. Parallel Optimization
    5. NCAA Fallback Configuration
    6. Feeder Leagues
  7. Caching Strategy
    1. Team Data Cache
    2. Color Cache
    3. Image Cache
    4. Cache Invalidation
  8. Team Matching System
    1. Normalization
    2. Location Abbreviations
    3. Matching Scores
    4. Special Cases
  9. Route Loading System
    1. Priority-Based Loading
    2. Why Priority Matters
  10. Data Sources
    1. League Logos
  11. Rate Limiting
    1. Configuration
    2. Scope
    3. Trust Proxy
  12. Error Handling
    1. Team Not Found
    2. Timeout Handling
    3. Development Mode
  13. Performance Optimizations
    1. Image Caching
    2. API Request Optimization
    3. Memory Management
  14. Logging
    1. Console Logging
    2. File Logging
  15. CORS Configuration
  16. Security Considerations
    1. Input Validation
    2. Image Proxying
    3. Rate Limiting
  17. Future Enhancements

Architecture Overview

Game Thumbs is a Node.js Express application that dynamically generates sports matchup thumbnails and logos using team and athlete data from multiple providers (ESPN, TheSportsDB, HockeyTech, MLBStats).

Key Components

  • Express Server: HTTP server handling API requests
  • Provider System: Modular data provider architecture with multiple providers
  • Team Matching: Intelligent fuzzy matching with weighted scoring
  • Image Generation: Canvas-based rendering with multiple visual styles
  • Caching System: Multi-layer caching for performance optimization
  • Color Extraction: Automatic dominant color detection from logos

Data Providers

Game Thumbs uses a modular provider architecture to fetch team and athlete data from multiple sources.

ESPN Provider

Leagues: NBA, NFL, MLB, NHL, NCAA, Soccer, and more
Type: Team-based sports
Features:

  • Fetches team rosters, logos, and colors from ESPN’s public APIs
  • Supports 30+ professional and NCAA leagues
  • 24-hour team data caching
  • Automatic logo and color extraction

ESPN Athlete Provider

Leagues: UFC, PFL, Bellator (MMA), Tennis (ATP/WTA)
Type: Athlete-based sports
Features:

  • Treats individual fighters/athletes as “teams” for matchup generation
  • Fetches complete athlete rosters from ESPN Sports Core API v2
  • 72-hour athlete data caching with automatic background refresh
  • Persistent file-based caching in .cache/providers/ for data retention across server restarts
  • Smart athlete matching by first name, last name, or full name
  • Sport-specific color palettes:
    • Tennis: Grass greens, clay browns/oranges, hard court blues, tennis ball yellow-greens
    • MMA: Dark blue palette
  • Headshot images used as athlete “logos”
  • Country flags used as fallback when headshots are unavailable
  • Doubles/Team Support: Use + separator to create composite athlete teams (e.g., djokovic+federer)
  • Supports 1,800+ UFC fighters, 500+ PFL fighters, 1,000+ Bellator fighters, 33,800+ tennis players (ATP & WTA)
  • Exponential backoff retry with 15s initial delay for rate limit handling
  • Conservative request throttling (25 concurrent requests per batch, 200ms delays)

Tennis Cache Time: Initial cache population for tennis (33,800+ athletes) takes 5-30 minutes due to ESPN API rate limiting. Cache persists to disk, so subsequent restarts are instant. The service is fully functional during cache population - it will search available data and complete the cache in the background.

Cache Auto-Refresh: The ESPN Athlete provider automatically refreshes athlete rosters at 95% of cache duration (68.4 hours) to ensure data stays fresh without requiring manual intervention or server restarts.

Doubles/Team Composite Generation: When multiple athletes are specified with +, the provider:

  1. Resolves each athlete individually
  2. Creates a composite image with athletes side-by-side (5px spacing)
  3. Merges names with / separator (e.g., “Djokovic / Federer”)
  4. Returns composite as a single “team” object for thumbnail generation

TheSportsDB Provider

Leagues: CFL, AHL, OHL, WHL, QMJHL, and international soccer
Type: Team-based sports
Features:

  • Community-maintained sports database
  • Good coverage for non-US leagues
  • Team colors, logos, and basic information
  • 24-hour team data caching

HockeyTech Provider

Leagues: PWHL, CHL, OHL, WHL Type: Team-based sports (hockey) Features:

  • Official provider for Canadian hockey leagues
  • Real-time roster data
  • High-quality team information
  • 24-hour team data caching

MLBStats Provider

Leagues: MiLB (Triple-A, Double-A, High-A, Single-A), Winter Leagues, Independent Leagues, KBO Type: Team-based sports (baseball) API: statsapi.mlb.com/api/v1 Features:

  • Free MLB StatsAPI — no API key required
  • Covers all MiLB levels and international baseball leagues tracked by MLB
  • SVG logos rasterized to PNG via svgUtils
  • Automatic color extraction from team logos
  • Season fallback: tries current year, then previous two years
  • 24-hour team data caching, 7-day logo caching
  • Configurable via sportId in league config

FlagCDN Provider

Leagues: Country, Olympics
Type: International country-based matchups
Features:

  • High-resolution flag images (2560px) from flagcdn.com
  • ISO 3166 alpha-2 and alpha-3 code support (USA, CAN, GBR, etc.)
  • Olympic/sports team codes (ROC, OAR, AOR, RPC)
  • UK home nations support (ENG, SCT, WAL, NIR)
  • Custom color extraction without filtering white colors
  • Desaturation (40%) and darkening (30%) for better thumbnail backgrounds
  • White color replacement (uses non-white color for both if either is white)
  • 7-day caching for country data and extracted colors
  • Smart country matching with weighted scoring

Country Resolution: Matches country names, aliases, and ISO codes using intelligent scoring:

  • ISO 3-letter codes: 1.0 weight (highest priority)
  • Exact name matches: 0.9 weight
  • Partial name matches: 0.5-0.8 weight based on similarity

Provider System

Automatic Provider Discovery

Game Thumbs automatically discovers and registers all providers from the providers/ directory at startup. No manual registration required.

How it works:

  1. Scans providers/ directory for *Provider.js files
  2. Excludes BaseProvider.js (abstract base class)
  3. Automatically instantiates and registers each provider
  4. Maps supported leagues to providers

Adding New Providers:

  1. Create a new file in providers/ following the naming convention: YourNameProvider.js
  2. Extend BaseProvider and implement required methods:
    • getProviderId(): Return unique provider identifier
    • resolveTeam(): Implement team/athlete resolution logic
    • getLeagueLogoUrl(): Implement league logo fetching
  3. Provider is automatically loaded on server restart

Provider Inference: The system automatically infers which provider to use based on the configuration object keys:

  • { espn: {...} } → ESPN Provider
  • { theSportsDB: {...} } → TheSportsDB Provider
  • { hockeytech: {...} } → HockeyTech Provider
  • { mlbStats: {...} } → MLBStats Provider
  • { espnAthlete: {...} } → ESPN Athlete Provider
  • { flagcdn: {...} } → FlagCDN Provider

No hardcoded provider lists to maintain!


Image Generation

Canvas Rendering

Images are generated using the Node.js canvas library, which provides a Cairo-backed implementation of the HTML5 Canvas API.

Features:

  • Server-side image rendering
  • Support for multiple image formats (PNG, JPEG)
  • Text rendering with custom fonts
  • Image compositing and transformations
  • Gradient fills and patterns

Visual Styles

Thumbnail & Cover Styles

  • Style 1: Diagonal/horizontal split with team colors
  • Style 2: Gradient blend between team colors
  • Style 3: Minimalist badge with team circles (light background)
  • Style 4: Minimalist badge with team circles (dark background)
  • Style 5: Grid background with grey diagonal lines and fade to black
  • Style 6: Grid background with team color gradient lines and fade to black
  • Style 98: 3D embossed design with textures, reflections, metallic VS badge, and league logo
  • Style 99: 3D embossed design with textures, reflections, and metallic VS badge (no league logo)

Logo Styles

  • Style 1: Diagonal split with dividing line
  • Style 2: Side by side arrangement
  • Style 3: Circle badges with team colors (league logo overlay)
  • Style 4: Square badges with team colors (league logo overlay)
  • Style 5: Circle badges with league logo on left (white background)
  • Style 6: Square badges with league logo on left (white background)

Logo Selection

The system automatically selects the best logo variant for each context:

  • Dark backgrounds: Uses light/default logos
  • Light backgrounds: Uses dark variant logos when available
  • Team color backgrounds: Selects variant with best contrast

Aspect Ratio Preservation

All league and team logos maintain their aspect ratio:

  • Logos are scaled to fit within designated areas
  • Transparent backgrounds are preserved
  • No distortion or stretching occurs

Color Extraction

When ESPN doesn’t provide team colors, the system automatically extracts them from team logos.

Process

  1. Download Logo: Fetches the team logo image
  2. Analyze Pixels: Processes pixel data to find dominant colors
  3. Filter Colors: Removes neutral/grayscale colors
  4. Ensure Distinctness: Verifies selected colors are visually distinct
  5. Cache Results: Stores extracted colors for 24 hours

Algorithm

  • Uses k-means clustering to identify dominant colors
  • Filters out colors with low saturation (grayscale)
  • Ensures minimum color distance between primary and alternate colors
  • Prioritizes vibrant, saturated colors

Fallback

If color extraction fails or no vibrant colors are found:

  • Primary Color: #000000 (black)
  • Alternate Color: #ffffff (white)

Team Resolution & Fallback System

Game Thumbs implements a sophisticated multi-layer fallback system to ensure requests succeed even when teams cannot be found in the primary data source.

Resolution Chain

When resolving a team, the system tries multiple approaches for optimal performance:

  1. Custom Teams: Checks if team is marked as custom: true in teams.json (bypasses all provider lookups)
  2. Primary Provider: Attempts to resolve team from the configured provider(s)
  3. Alternate Providers (parallel): If team found but has no logo, tries all other providers simultaneously
  4. Feeder Leagues (parallel): Searches all configured feeder leagues at once
  5. Fallback League: Falls back to a designated league (e.g., NCAA sports → Men’s Basketball)
  6. Greyscale League Logo: If fallback=true parameter is set, uses greyscale league logo as placeholder
  7. Ultimate Text Fallback: If league logo fails, generates minimal single-letter placeholder on transparent background

Custom Teams (No Provider Lookup)

Teams can be defined as “custom teams” in teams.json with the custom: true flag. These teams bypass all provider lookups entirely:

{
  "nfl": {
    "nfc": {
      "custom": true,
      "override": {
        "name": "National Football Conference",
        "abbreviation": "NFC",
        "logoUrl": "https://a.espncdn.com/i/teamlogos/nfl/500/scoreboard/nfc.png",
        "color": "#013369",
        "alternateColor": "#D50A0A"
      }
    }
  }
}

Required Fields:

  • name - Team display name
  • abbreviation - Team abbreviation
  • logoUrl - Team logo URL

Optional Fields (auto-extracted from logo):

  • color - Primary color (extracted from logo if omitted)
  • alternateColor - Secondary color (extracted from logo if omitted)

Use Cases:

  • Conference/Division teams (NFC vs AFC)
  • All-Star teams (Pro Bowl rosters)
  • Special event teams
  • Historical teams not in current data
  • Fantasy/custom league teams

Resolution Priority: Custom teams are checked first before any provider queries, ensuring instant resolution with zero external API calls.

Unsupported League Fallback (Provider Interface)

When a league is not configured in leagues.json, providers can optionally support it through the unconfigured league interface:

  1. Provider Implementation: Providers implement canHandleUnconfiguredLeague() and getUnconfiguredLeagueConfig() methods
  2. Automatic Lookup: When findLeague() doesn’t find a configured league, it queries ProviderManager.findUnconfiguredLeague()
  3. Provider Check: ProviderManager iterates through all registered providers to find one that can handle the league
  4. Temporary League Object: The supporting provider creates a temporary league configuration on-the-fly
  5. Normal Processing: The request continues through the normal flow using that provider

Example (ESPN Provider):

  • Request: GET /eng.w.1/team1/team2/thumb
  • findLeague('eng.w.1') checks configured leagues → not found
  • Calls providerManager.findUnconfiguredLeague('eng.w.1')
  • ProviderManager queries each provider’s canHandleUnconfiguredLeague() method
  • ESPN provider’s cache finds eng.w.1 exists under sport soccer
  • Returns temporary league object with ESPN provider configuration
  • ESPN provider resolves teams and generates thumbnail normally
  • Result: Successfully generated matchup thumbnail

This architecture allows any provider to support unconfigured leagues, not just ESPN. The fallback is completely automatic—no special query parameters needed.

ESPN Implementation: The ESPN provider caches all available sports/leagues from ESPN’s Core API on startup (200-400+ leagues across 17+ sports). The cache is self-contained within ESPNProvider.js, similar to how ESPNAthleteProvider.js manages its athlete cache. Initial discovery takes 1-2 minutes but runs asynchronously without blocking the server.

Parallel Optimization

Team Pair Resolution: Both teams in a matchup are resolved simultaneously, cutting resolution time in half.

Provider Checks: All providers for a league are queried in parallel using Promise.all().

Feeder League Checks: All feeder leagues are searched simultaneously rather than sequentially.

Shared League Logo Processing: When both teams in a matchup fail, the league logo is downloaded and processed only once, then reused for both teams.

NCAA Fallback Configuration

All NCAA sports use NCAA Men’s Basketball (ncaam) as their fallback league, as it has the most comprehensive roster. Additionally, a generic ncaa league is available that also falls back to ncaam:

{
  "ncaa": {
    "name": "NCAA",
    "logoUrl": "./assets/NCAA.png",
    "fallbackLeague": "ncaam"
  },
  "ncaavb": {
    "name": "NCAA Men's Volleyball",
    "fallbackLeague": "ncaam"
  }
}

This ensures that even obscure NCAA teams can be found if they participate in basketball. The generic ncaa league allows teams to use /ncaa/team1/team2/thumb without specifying a particular sport.

Feeder Leagues

Leagues can configure feeder leagues that are automatically searched:

Example - English Premier League:

{
  "epl": {
    "feederLeagues": ["championship", "league1", "league2"]
  }
}

This enables finding promoted/relegated teams without changing the league code.

Example - Tennis:

{
  "tennis": {
    "feederLeagues": ["atp", "wta"]
  }
}

Allows unified tennis endpoint while searching both ATP and WTA rosters.


Caching Strategy

The application implements multi-layer caching to optimize performance and reduce API calls.

Team Data Cache

  • Duration: 24 hours (configurable via IMAGE_CACHE_HOURS)
  • Scope: Per league
  • Storage: In-memory cache
  • Key Format: {league}_teams

Color Cache

  • Duration: 24 hours
  • Scope: Per team
  • Storage: In-memory cache with size limits
  • Key Format: colors_{teamId}
  • Max Size: 1000 entries (oldest entries removed when exceeded)

Image Cache

  • Duration: 24 hours (configurable via IMAGE_CACHE_HOURS)
  • Scope: Per unique image request
  • Storage: In-memory buffer cache
  • Key Format: Content-based hash of parameters

Cache Invalidation

  • Automatic cleanup of expired entries
  • Manual cache clearing available via provider methods
  • Restart server to clear all caches

Team Matching System

Normalization

Text is normalized for matching using two approaches:

  1. Standard Normalization:
    • Remove accents and diacritical marks (e.g., “Montréal” → “Montreal”)
    • Convert to lowercase
    • Replace non-alphanumeric characters with spaces
    • Collapse multiple spaces
    • Trim whitespace
  2. Compact Normalization:
    • Remove accents and diacritical marks
    • Convert to lowercase
    • Remove all non-alphanumeric characters
    • No spaces (e.g., “Los Angeles” → “losangeles”)

Accent Handling: The system uses Unicode NFD normalization to strip diacritical marks, allowing teams like “Atlético Madrid”, “São Paulo”, or “Montréal” to match with or without accents.

Location Abbreviations

The system recognizes common location abbreviations:

Abbreviation Expansions
la Los Angeles, LA
ny, nyc New York, NYC
sf San Francisco, SF
dc Washington, DC
chi Chicago, Chi
atl Atlanta, Atl

And many more. See teamUtils.js for the complete list.

Matching Scores

Teams are scored based on how well they match the input:

Match Type Score
Custom alias (exact) 1000
Abbreviation (exact) 1000
Team name (exact) 900
Short display name (exact) 850
Full name (exact) 800
Team name (partial contains) 700
City (exact) 500
Location+Team concatenation 950
City (partial) 100

Priority Order: Team name matches are now prioritized over exact city matches (700 vs 500) to prevent false matches where a city name inadvertently matches a different team.

The team with the highest score is selected. Teams with zero score are rejected.

Special Cases

  • Location prefixes: “losangelesfc” matches “LAFC” (LA + FC)
  • Flexible spacing: “Man Utd” matches “manutd”
  • Case insensitive: “LAKERS” matches “lakers”
  • Accent insensitive: “Atlético” matches “Atletico”, “Montréal” matches “Montreal”
  • Unicode normalization: All diacritical marks are stripped during matching

Route Loading System

Priority-Based Loading

Routes are loaded in a specific order to prevent conflicts between similar patterns:

  1. Priority Routes: Routes with an explicit priority field are loaded first (lower numbers = higher priority)
  2. Alphabetical Routes: Routes without a priority field are loaded alphabetically

Example:

module.exports = {
    priority: 1,  // Load before other routes
    paths: ["/ncaa/:sport/:type"],
    method: "get",
    handler: async (req, res) => { ... }
}

Why Priority Matters

The NCAA shorthand route (/ncaa/:sport/:type) needs to be registered before the unified routes (/:league/:team1/:type) to prevent the more generic pattern from matching NCAA requests first. The priority system ensures:

  • NCAA routes are registered first (priority: 1)
  • Other routes load alphabetically (no priority field = lowest priority)
  • No route conflicts or unexpected matching

NCAA Route Fallthrough: When the NCAA route doesn’t recognize a sport shorthand (e.g., /ncaa/team1/team2/thumb), it calls next() to pass control to the unified route handler. This allows the generic ncaa league to work for team matchups without specifying a specific sport.


Data Sources

Team and athlete data is fetched from multiple providers based on league configuration. See the Data Providers section above for details on each provider’s API endpoints and data structure.

League Logos

ESPN Leagues:

  • Fetched from ESPN API or ESPN CDN
  • Format: https://a.espncdn.com/i/teamlogos/leagues/500/{league}.png

NCAA Sports:

  • Hosted on NCAA.com
  • Format: https://www.ncaa.com/modules/custom/casablanca_core/img/sportbanners/{sport}.png

Custom Leagues:

  • Configured via logoUrl or logoUrlDark in league configuration

Rate Limiting

Configuration

  • Default: 30 requests per minute per IP
  • Configurable: Set RATE_LIMIT_PER_MINUTE environment variable
  • Disable: Set RATE_LIMIT_PER_MINUTE=0

Scope

  • Image Generation: Rate limited at configured rate
  • Raw Data Endpoints: 3x the image generation rate limit
  • Cached Requests: Not rate limited (served from cache)

Trust Proxy

Set TRUST_PROXY to the number of proxies between the internet and your app:

  • Local dev: 0
  • Behind one proxy: 1
  • Behind multiple proxies: Number of proxy hops

This ensures accurate IP detection for rate limiting.


Error Handling

Team Not Found

When a team is not found, the API returns:

{
  "error": "Team not found: '{input}' in {LEAGUE}. Available teams: {list}"
}

Fallback Option:

Set fallback=true query parameter to handle missing teams gracefully using the multi-layer fallback system:

Single Team Endpoints:

GET /nba/invalidteam/thumb?fallback=true

Returns the NBA league thumbnail instead of an error.

Matchup Endpoints:

GET /nba/lakers/invalidteam/thumb?style=1&fallback=true

Generates the matchup using:

  • Lakers logo and colors for team 1
  • Greyscale league logo (35% opacity) with light grey colors (#d3d3d3, #b8b8b8) for the missing team
  • The selected style and options are preserved

Performance: Both teams in a matchup are resolved in parallel. If both teams fail and need the greyscale league logo, it’s downloaded and processed only once, then reused for both teams.

Ultimate Fallback: If the league logo itself is invalid (SVG, HTML, etc.), the system generates a minimal single-letter placeholder on a transparent background as the final safety net.

This allows matchup generation to continue even when one or both teams are not found, using a subtle placeholder that clearly indicates missing data while maintaining the overall design aesthetic.

How It Works Internally:

The system uses a shared handleTeamNotFoundError() utility across all route handlers to ensure consistent fallback behavior. The resolution process tries:

Timeout Handling

  • Request Timeout: 10 seconds (configurable via REQUEST_TIMEOUT)
  • Server Timeout: 30 seconds (configurable via SERVER_TIMEOUT)

Prevents hanging on slow/unresponsive external services.

Development Mode

Set NODE_ENV=development for:

  • Detailed error messages
  • Full stack traces in responses
  • Additional logging

Performance Optimizations

Image Caching

  • Content-based cache keys (same parameters = same cached image)
  • In-memory storage for fast retrieval
  • Automatic cleanup of expired entries

API Request Optimization

  • Batch team data fetching (all teams per league in one request)
  • 24-hour cache prevents repeated API calls
  • Timeout handling prevents resource exhaustion

Memory Management

  • Color cache has size limits (max 1000 entries)
  • Automatic cleanup of oldest entries when limit exceeded
  • Efficient buffer management for images

Logging

Console Logging

  • Colored output (configurable via FORCE_COLOR)
  • Timestamps (configurable via SHOW_TIMESTAMP)
  • Request logging with response times
  • Error logging with stack traces (in development mode)

File Logging

Enable with LOG_TO_FILE=true:

  • Location: ./logs directory
  • Format: app-YYYY-MM-DD-NNN.log
  • Rotation: ~100KB per file
  • Retention: Configurable via MAX_LOG_FILES (default: 10)
  • Content: Full timestamps and stack traces (always)

CORS Configuration

  • Default: Allow all origins (*)
  • Configurable: Set CORS_ORIGIN to specific domain
  • Max Age: 24 hours (configurable via CORS_MAX_AGE)

Security Considerations

Input Validation

  • League codes validated against supported list
  • Team identifiers sanitized before use
  • Query parameters validated and typed

Image Proxying

  • Team and league logos are proxied through the server
  • Prevents direct client access to external URLs
  • Ensures consistent caching behavior

Rate Limiting

  • Prevents abuse and resource exhaustion
  • IP-based limits with proxy support
  • Separate limits for different endpoint types

Future Enhancements

Potential areas for expansion:

  • Additional combat sports (Boxing, Wrestling, etc.)
  • More visual styles and customization options
  • Image format options (JPEG, WebP, SVG)
  • Persistent cache storage (Redis, filesystem)
  • Advanced team matching with ML/AI
  • Historical data and archive support
  • Real-time game score integration
  • Custom font and typography options
  • Fighter statistics and rankings display