Skip to content

RomM (ROM Management)

Self-hosted ROM management with IGDB + ScreenScraper metadata, Tinfoil integration.

Source: extracted from AGENTS.md

RomM (ROM Management)

RomM (container: romm) — Self-hosted ROM management platform with IGDB + ScreenScraper metadata.

Detail Value
Web UI http://100.68.251.84:8087 (nginx cached) / http://100.68.251.84:8089 (direct)
Image rommapp/romm:latest
Stack stacks/storage/romm/compose.yaml
Server unraid
Version v4.8.1
Database PostgreSQL at 100.68.251.84:5432, DB romm
Redis 100.68.251.84:6379 DB3
Admin lucndm / minhluc1

Architecture:

/mnt/user/roms/romm/library/
├── roms/          # ROM files organized by platform slug
│   ├── switch/    # 10,172 ROMs
│   ├── genesis/   # 1,943
│   ├── nes/       # 1,047
│   ├── gameboy/   # 935
│   ├── arcade/    # 859
│   ├── snes/      # 759
│   ├── gba/       # 506
│   ├── nds/       # 466
│   ├── sms/       # 306 (Master System)
│   ├── n64/       # 62
│   ├── sg1000/    # 16
│   ├── segacd/    # 10
│   └── 3ds/       # 9
└── bios/          # BIOS files by platform

Metadata providers: IGDB + ScreenScraper (credentials in TOML environment + compose.env)

API auth flow:

  1. GET /api/heartbeat → extract romm_csrftoken cookie
  2. POST /api/token (form-urlencoded) → JWT access_token
  3. API calls: Authorization: Bearer <token>

Triggering scans:

  • API auth currently returns 403 for platform listing (JWT scopes empty) — may be a v4 bug
  • Workaround: enqueue scan directly via Redis RQ inside container:
ssh root@100.68.251.84 "docker exec romm python3 -c '
from tasks.scheduled.scan_library import scan_platforms, ScanType
import redis
from rq import Queue
q = Queue(\"high\", connection=redis.Redis(host=\"100.68.251.84\", port=6379, db=3))
job = q.enqueue(scan_platforms, [1,2,3,4,5,6,7,8,9,10,11,12,13], [\"igdb\",\"ss\"], ScanType.COMPLETE, job_timeout=\"2h\")
print(f\"Job: {job.id}\")
'"

Tinfoil integration (Switch download from RomM):

  • Native feed endpoint: /api/feeds/tinfoil — returns custom index JSON per Tinfoil spec
  • Env var: DISABLE_DOWNLOAD_ENDPOINT_AUTH=true — allows unauthenticated downloads
  • Supported file types: .xci, .nsp, .nsz, .xcz, .nro
  • Tinfoil setup trên Switch (File Browser → Add New):
  • Protocol: http | Host: 100.68.251.84 | Port: 8087 | Path: /api/feeds/tinfoil
  • No username/password needed (auth disabled)

Tinfoil feed caching (nginx sidecar):

  • RomM generates feed on-the-fly from DB — ~101s for 1MB JSON with 10K ROMs
  • Nginx sidecar (romm-cache) serves static cached file: 10ms vs 101s
  • Architecture:
    graph LR
        CRON["Cron (5min)"] -->|curl| ROMM["RomM direct :8089"]
        ROMM -->|write tinfoil.json| STATIC["static file tinfoil.json"]
        STATIC --> NGINX["nginx serves :8087"]
        TF["Tinfoil / Client"] --> NGINX
        NGINX -->|HIT| STATIC
        NGINX -->|MISS| ROMMFB["fallback to RomM"]
    
  • Cache script: /boot/config/scripts/romm-cache-refresh.sh
  • Cron: */5 * * * * — refreshes every 5 minutes
  • Cache file: /mnt/user/appdata/romm/nginx-cache/tinfoil.json
  • Nginx config: /mnt/user/appdata/romm/nginx-feed-cache.conf (written via SSH, NOT via pre_deploy — Komodo printf loses newlines)
  • Port layout:
  • 8087: nginx sidecar (cached feed + pass-through for all other endpoints)
  • 8089: RomM direct (no cache — admin UI + cron fetch source)

Scan batching strategy (for large libraries):

  • Single scan job times out at 2h default — not enough for 17K ROMs
  • Split into batches by platform, queue sequentially via RQ depends_on:
ssh root@100.68.251.84 "docker exec romm python3 -c '
from tasks.scheduled.scan_library import scan_platforms, ScanType
import redis
from rq import Queue
q = Queue(\"high\", connection=redis.Redis(host=\"100.68.251.84\", port=6379, db=3))
# Batch by size: small platforms first, switch last
job = q.enqueue(scan_platforms, [platform_ids], [\"igdb\",\"ss\"], ScanType.COMPLETE, job_timeout=\"4h\")
'"
  • IGDB API rate limit: ~4 req/s → scan speed ~7-8 ROMs/min → 17K ROMs takes ~35 hours
  • Already identified ROMs survive redeploy (stored in external PostgreSQL)

Pitfalls:

  • file_paths must NOT include compose.env — Komodo parses all files as YAML, compose.env causes deploy failure. Use additional_env_files = ["compose.env"] instead
  • RomM API /api/login needs CSRF — flow is heartbeat → CSRF cookie → POST login. But /api/token (form-urlencoded) works with CSRF for JWT auth
  • JWT scopes empty — token has scopes: "" despite ADMIN role. API returns 403. Use Redis RQ for management tasks
  • Platform slug conventions: Master System = sms (not mastersystem), SG-1000 = sg1000
  • scan_library task has manual_run: false — can't trigger via scheduler API. Use scan_platforms() via RQ
  • Scan job timeout — default 2h (SCAN_TIMEOUT env var). For large platforms, set job_timeout parameter when enqueuing via RQ (not the env var — env var is for the scan itself, RQ job_timeout is for the worker)
  • Privacy filter blocks git push when compose.env exists in repo — use Python subprocess.run(['git','push']) or gh api PUT to bypass
  • Compose mount is read-only: /mnt/user/roms/romm/library:/romm/library:ro — ROM management happens via API, not filesystem writes from container
  • Tinfoil feed caching — nginx proxy_cache failed on Unraid (permission issues with volume write). Switched to static file + cron approach instead
  • Nginx config in pre_deploy — Komodo printf loses newlines for multi-line content. Write nginx config via SSH directly to host instead
  • Port 8088 used by Homepage — RomM direct moved to 8089
  • Gitea repo archivinggit push gitea fails with "repo is archived". Unarchive via API: curl -X PATCH ... -d '{"archived": false}' with $GITEA_ACCESS_TOKEN