Files
romm/docs/BACKEND_ARCHITECTURE.md

1780 lines
92 KiB
Markdown

# RomM Backend Architecture
Comprehensive documentation of the RomM backend: a FastAPI-based server powering the self-hosted retro gaming platform.
---
## Table of Contents
1. [Overview](#1-overview)
2. [High-Level Architecture](#2-high-level-architecture)
3. [Directory Structure](#3-directory-structure)
4. [Application Lifecycle](#4-application-lifecycle)
5. [Database Layer](#5-database-layer)
6. [API Endpoints](#6-api-endpoints)
7. [Authentication & Authorization](#7-authentication--authorization)
8. [Business Logic (Handlers)](#8-business-logic-handlers)
9. [External Integrations (Adapters)](#9-external-integrations-adapters)
10. [Real-Time Communication (WebSockets)](#10-real-time-communication-websockets)
11. [Background Tasks & Scheduling](#11-background-tasks--scheduling)
12. [File System Management](#12-file-system-management)
13. [Caching (Redis)](#13-caching-redis)
14. [Configuration](#14-configuration)
15. [Error Handling](#15-error-handling)
16. [Logging](#16-logging)
17. [Testing](#17-testing)
---
## 1. Overview
| Property | Value |
| ------------------ | -------------------------------- |
| **Framework** | FastAPI 0.121.1 |
| **Language** | Python 3.13+ |
| **ORM** | SQLAlchemy 2.0 |
| **Migrations** | Alembic |
| **Databases** | MariaDB, MySQL, PostgreSQL |
| **Cache/Queue** | Redis (via RQ) |
| **Real-time** | Socket.IO (python-socketio) |
| **Auth** | OAuth2 + Basic + OIDC + Sessions |
| **ASGI Server** | Uvicorn / Gunicorn |
| **Error Tracking** | Sentry |
RomM's backend is responsible for:
- **Library scanning**: detecting platforms and ROMs from the filesystem
- **Metadata enrichment**: pulling game info from 10+ external providers
- **User management**: roles, authentication, per-user game tracking
- **Asset management**: saves, save states, screenshots, firmware/BIOS
- **Device sync**: cross-device save synchronization
- **Netplay**: real-time multiplayer room coordination
- **Feed generation**: Tinfoil, WebRcade, PKGi, and other custom formats
---
## 2. High-Level Architecture
```text
+---------------------+
| Nginx / CDN |
| (reverse proxy, |
| X-Accel-Redirect) |
+----------+----------+
|
v
+-------------+--------------+
| FastAPI Application |
| (main.py) |
+-------------+--------------+
|
+-----------------------------+------------------------------+
| | |
+--------v--------+ +---------v----------+ +---------v--------+
| Middleware | | API Routers | | WebSockets |
| Stack (5 layers)| | (20 routers) | | /ws /netplay |
+---------+--------+ +---------+----------+ +---------+--------+
| | |
v v v
+-------------------+ +-----------+-----------+ +----------+---------+
| CORS | | Endpoint Layer | | Socket.IO Server |
| CSRF | | (request validation, | | (scan progress, |
| Authentication | | response schemas) | | netplay rooms) |
| Session (Redis) | +-----------+-----------+ +----------+---------+
| Context Vars | | |
+-------------------+ v |
+-------------+--------------+ |
| Handler Layer | |
| (business logic, CRUD, |<--------------+
| metadata, filesystem) |
+------+-------+------+------+
| | |
+------------------+ | +------------------+
| | |
+--------v--------+ +----------v----------+ +---------v---------+
| Database | | External APIs | | File System |
| (SQLAlchemy) | | (IGDB, MobyGames, | | (ROM library, |
| | | ScreenScraper, | | assets, BIOS) |
| MariaDB/MySQL/PG | | SteamGridDB, RA, | | |
+---------+--------+ | LaunchBox, HLTB, | +-------------------+
| | Hasheous, TGDB, |
v | Flashpoint) |
+-------------------+ +---------------------+
| Redis |
| (sessions, cache, |
| job queues, rooms)|
+-------------------+
```
### Layered Architecture
```text
+-----------------------------------------------------------------------+
| PRESENTATION LAYER |
| endpoints/ API route handlers, request/response schemas |
| endpoints/sockets/ WebSocket event handlers |
+-----------------------------------------------------------------------+
| BUSINESS LOGIC LAYER |
| handler/auth/ Authentication & authorization |
| handler/database/ CRUD operations per entity |
| handler/metadata/ Metadata fetching & normalization |
| handler/filesystem/ File system operations |
| handler/ Scan orchestration, netplay, socket management |
+-----------------------------------------------------------------------+
| DATA ACCESS LAYER |
| models/ SQLAlchemy ORM model definitions |
| adapters/services/ External API client wrappers |
| handler/redis_handler.py Cache & queue operations |
+-----------------------------------------------------------------------+
| INFRASTRUCTURE LAYER |
| config/ Environment variables & YAML config |
| decorators/ Auth & DB session decorators |
| exceptions/ Custom exception hierarchy |
| logger/ Structured logging |
| utils/ Shared helpers (hashing, context, validation) |
| tasks/ Background job definitions & scheduling |
+-----------------------------------------------------------------------+
```
---
## 3. Directory Structure
```text
backend/
├── main.py # FastAPI app creation, middleware, routers
├── startup.py # Pre-startup: cache init, scheduled tasks
├── watcher.py # Filesystem change detection (watchfiles)
├── __version__.py # Version placeholder
├── alembic.ini # Alembic migration configuration
├── pytest.ini # Test configuration
├── .coveragerc # Code coverage settings
├── adapters/ # External API client wrappers
│ └── services/
│ ├── igdb.py # IGDB (Internet Game Database)
│ ├── mobygames.py # MobyGames
│ ├── screenscraper.py # ScreenScraper
│ ├── steamgriddb.py # SteamGridDB
│ ├── retroachievements.py # RetroAchievements
│ ├── rahasher.py # RA hash computation
│ ├── libretro_thumbnails.py # Libretro thumbnails (covers/screenshots)
│ └── *_types.py # Type definitions per adapter
├── alembic/ # Database migrations
│ ├── env.py # Migration environment setup
│ └── versions/ # 80+ migration scripts
├── config/ # Configuration system
│ ├── __init__.py # Env var loading (100+ variables)
│ └── config_manager.py # YAML config manager (singleton)
├── decorators/ # Function decorators
│ ├── auth.py # @protected_route, OAuth setup
│ └── database.py # @begin_session (DB session injection)
├── endpoints/ # API route handlers
│ ├── auth.py # Login, logout, token, OIDC
│ ├── user.py # User CRUD, invite links
│ ├── client_tokens.py # API token management
│ ├── platform.py # Platform CRUD
│ ├── collections.py # Collection management
│ ├── configs.py # App configuration
│ ├── device.py # Device registration
│ ├── export.py # ES-DE gamelist.xml + Pegasus exports
│ ├── feeds.py # Tinfoil, WebRcade, PKGi feeds
│ ├── firmware.py # BIOS/firmware management
│ ├── heartbeat.py # Health check + setup wizard
│ ├── netplay.py # Netplay room listing
│ ├── play_sessions.py # Play session ingestion & queries
│ ├── raw.py # Raw asset file serving
│ ├── saves.py # Save file management
│ ├── screenshots.py # Screenshot management
│ ├── search.py # Cross-provider metadata search
│ ├── states.py # Save state management
│ ├── stats.py # Library statistics
│ ├── sync.py # Device sync sessions (push/pull, SSH)
│ ├── tasks.py # Task monitoring & triggering
│ ├── roms/ # ROM-specific endpoints
│ │ ├── __init__.py # ROM CRUD, download, bulk ops
│ │ ├── upload.py # Chunked upload system
│ │ ├── files.py # File download (nginx redirect)
│ │ ├── manual.py # Manual metadata entry
│ │ └── notes.py # ROM notes/comments
│ ├── sockets/ # WebSocket handlers
│ │ ├── scan.py # Scan progress events
│ │ └── netplay.py # Netplay room events
│ ├── forms/ # Request body models
│ │ └── identity.py # Auth form schemas
│ └── responses/ # Pydantic response schemas
│ ├── base.py # Base response classes
│ ├── rom.py # SimpleRomSchema, DetailedRomSchema
│ ├── platform.py # PlatformSchema
│ ├── identity.py # UserSchema, TokenResponse
│ └── ... # 15+ more response schemas
├── exceptions/ # Custom exception classes
│ ├── auth_exceptions.py # AuthCredentialsException, etc.
│ ├── endpoint_exceptions.py # NotFound, Permission, Conflict
│ ├── fs_exceptions.py # Filesystem errors
│ ├── config_exceptions.py # Config write errors
│ ├── task_exceptions.py # Scheduler errors
│ └── socket_exceptions.py # Scan stopped
├── handler/ # Business logic layer
│ ├── scan_handler.py # Library scan orchestration
│ ├── socket_handler.py # Socket.IO server management
│ ├── netplay_handler.py # Netplay room state
│ ├── redis_handler.py # Redis clients & queues
│ ├── auth/ # Authentication subsystem
│ │ ├── base_handler.py # Auth, OAuth, OIDC handlers
│ │ ├── hybrid_auth.py # Multi-method auth backend
│ │ ├── constants.py # Scopes, role mappings
│ │ └── middleware/ # CSRF, session middleware
│ ├── database/ # Per-entity CRUD handlers
│ │ ├── base_handler.py # Engine, session factory
│ │ ├── roms_handler.py # ROM queries & mutations
│ │ ├── platforms_handler.py # Platform CRUD, slug mapping
│ │ ├── users_handler.py # User CRUD, role management
│ │ ├── saves_handler.py # Save slot grouping, sync state
│ │ ├── states_handler.py # Save state CRUD
│ │ ├── screenshots_handler.py # Screenshot CRUD
│ │ ├── firmware_handler.py # BIOS/firmware CRUD
│ │ ├── collections_handler.py # Regular/smart/virtual collections
│ │ ├── devices_handler.py # Device registration, fingerprinting
│ │ ├── device_save_sync_handler.py # Cross-device save sync state
│ │ ├── client_tokens_handler.py # API token hash lookup
│ │ ├── play_sessions_handler.py # Play session ingest & aggregation
│ │ ├── sync_sessions_handler.py # Sync session lifecycle
│ │ └── stats_handler.py # Library statistics
│ ├── filesystem/ # File I/O operations
│ │ ├── base_handler.py # Path helpers, base class
│ │ ├── roms_handler.py # ROM file reading, hashing
│ │ ├── assets_handler.py # User assets storage
│ │ ├── firmware_handler.py # BIOS file I/O
│ │ ├── platforms_handler.py # Platform folder detection
│ │ └── resources_handler.py # Artwork caching
│ └── metadata/ # Metadata provider handlers
│ ├── base_handler.py # Base metadata handler
│ ├── igdb_handler.py # IGDB provider
│ ├── moby_handler.py # MobyGames provider
│ ├── ss_handler.py # ScreenScraper
│ ├── sgdb_handler.py # SteamGridDB
│ ├── ra_handler.py # RetroAchievements
│ ├── hltb_handler.py # HowLongToBeat
│ ├── hasheous_handler.py # Hasheous hash-based lookup
│ ├── tgdb_handler.py # TheGamesDB
│ ├── flashpoint_handler.py # Flashpoint archive
│ ├── gamelist_handler.py # gamelist.xml parser
│ ├── libretro_handler.py # Libretro thumbnails DB lookup
│ ├── playmatch_handler.py # PlayMatch algorithm
│ ├── launchbox_handler/ # LaunchBox (local + remote)
│ └── fixtures/ # Static metadata indexes
│ ├── mame_index.json # MAME ROM → game info
│ ├── scummvm_index.json # ScummVM identification
│ ├── ps1_serial_index.json # PS1 serial → game
│ ├── ps2_serial_index.json # PS2 serial → game
│ ├── ps2_opl_index.json # PS2 OPL serials
│ └── psp_serial_index.json # PSP serial → game
├── logger/ # Logging setup
│ ├── logger.py # Logger instance ("romm")
│ └── formatter.py # Colored formatter, LOGGING_CONFIG
├── models/ # SQLAlchemy ORM models
│ ├── base.py # BaseModel (created_at, updated_at)
│ ├── user.py # User, Role enum
│ ├── platform.py # Platform
│ ├── rom.py # Rom, RomFile, RomMetadata, RomUser, RomNote
│ ├── collection.py # Collection, SmartCollection, VirtualCollection
│ ├── assets.py # Save, State, Screenshot
│ ├── device.py # Device, SyncMode enum
│ ├── device_save_sync.py # DeviceSaveSync
│ ├── firmware.py # Firmware
│ ├── client_token.py # ClientToken
│ ├── play_session.py # PlaySession (per-user playtime tracking)
│ ├── sync_session.py # SyncSession (device sync coordination)
│ └── fixtures/ # Seed data
│ └── known_bios_files.json # Verified BIOS hashes
├── tasks/ # Background job system
│ ├── tasks.py # Base Task, PeriodicTask classes
│ ├── scheduled/ # Cron-scheduled tasks
│ │ ├── scan_library.py # Nightly library rescan
│ │ ├── sync_retroachievements_progress.py # Pull RA user progress
│ │ ├── update_switch_titledb.py # Refresh Switch TitleDB
│ │ ├── update_launchbox_metadata.py # Refresh LaunchBox data
│ │ ├── convert_images_to_webp.py # Artwork WebP conversion
│ │ └── cleanup_netplay.py # Prune stale netplay rooms
│ └── manual/ # On-demand tasks
│ ├── cleanup_missing_roms.py # Drop DB entries for missing files
│ ├── cleanup_orphaned_resources.py # Remove unreferenced artwork
│ └── sync_folder_scan.py # Scan sync folder for new saves
├── utils/ # Shared helpers
│ ├── __init__.py # get_version()
│ ├── cache.py # Redis fixture loading
│ ├── hashing.py # CRC32, file hashing
│ ├── context.py # Async context vars (aiohttp, httpx)
│ ├── database.py # JSON/JSONB helpers, DB detection
│ ├── filesystem.py # Path sanitization
│ ├── validation.py # Input validation
│ ├── client_tokens.py # Token generation
│ ├── datetime.py # UTC helpers
│ ├── json_module.py # Custom JSON encoder
│ ├── nginx.py # X-Accel-Redirect responses
│ ├── router.py # Custom APIRouter
│ ├── gamelist_exporter.py # ES-DE gamelist.xml generation
│ ├── archive_7zip.py # 7-Zip archive handling
│ ├── platforms.py # Platform management
│ └── emoji.py # Emoji utilities
├── tools/ # Development utilities
│ └── xml_diagnostics.py # XML diagnostic tool
├── tests/ # Test suite
│ ├── conftest.py # Pytest fixtures
│ └── ... # Mirrors backend structure
└── romm_test/ # Test fixtures & data
├── assets/users/ # Test user saves
├── config/ # Test configuration
├── library/ # Test ROM library
└── resources/roms/ # Test ROM resources
```
---
## 4. Application Lifecycle
### Startup Sequence
```text
1. alembic upgrade head # Run database migrations
2. startup.main() # Async startup tasks
├── Initialize scheduled jobs (RQ Scheduler)
│ ├── cleanup_netplay
│ ├── scan_library (if ENABLE_SCHEDULED_RESCAN)
│ ├── update_switch_titledb
│ ├── update_launchbox_metadata
│ ├── convert_images_to_webp
│ └── sync_retroachievements_progress
└── Load fixture caches into Redis
├── mame_index.json
├── scummvm_index.json
├── ps1/ps2/psp serial indexes
└── known_bios_files.json
3. uvicorn.run("main:app") # Start ASGI server
└── FastAPI lifespan
├── Create aiohttp.ClientSession
├── Create httpx.AsyncClient
└── Store in app.state + context vars
```
### Middleware Stack (execution order, outside-in)
```text
Request → CORS → CSRF → Authentication → Session (Redis) → Context Vars → Endpoint
Response ← CORS ← CSRF ← Authentication ← Session (Redis) ← Context Vars ← Endpoint
```
| Layer | Middleware | Purpose |
| ----- | -------------------------- | ------------------------------------------------- |
| 1 | `CORSMiddleware` | Allow cross-origin requests (all origins) |
| 2 | `CSRFMiddleware` | Token-based CSRF protection (cookie + header) |
| 3 | `AuthenticationMiddleware` | `HybridAuthBackend`: Basic, Bearer, Session, OIDC |
| 4 | `RedisSessionMiddleware` | Cookie-based sessions stored in Redis |
| 5 | `set_context_middleware` | Inject aiohttp/httpx clients into context vars |
### Request Flow
```text
HTTP Request
├─ Middleware processes request (auth, session, CSRF)
├─ FastAPI routes to endpoint handler
│ └─ @protected_route checks scopes
├─ Endpoint calls handler layer
│ ├─ handler/database/* → SQLAlchemy queries
│ ├─ handler/metadata/* → External API calls
│ ├─ handler/filesystem/* → File I/O
│ └─ handler/auth/* → Token operations
├─ Response schema (Pydantic) serializes output
└─ HTTP Response
```
---
## 5. Database Layer
### Supported Databases
| Database | Driver | Status |
| ------------- | --------------------- | --------- |
| MariaDB 10.5+ | `mariadb+pymysql` | Default |
| MySQL 8.0+ | `mysql+pymysql` | Supported |
| PostgreSQL | `postgresql+psycopg2` | Supported |
### Engine & Session Setup
**Location:** `handler/database/base_handler.py`
```python
sync_engine = create_engine(
ConfigManager.get_db_engine(),
pool_pre_ping=True, # Connection health check
echo=False, # SQL logging (DEV_SQL_ECHO overrides)
)
sync_session = sessionmaker(bind=sync_engine, expire_on_commit=False)
```
Sessions are injected via the `@begin_session` decorator, which wraps handlers in a transaction context.
### Base Model
**Location:** `models/base.py`
All models inherit `BaseModel`, providing:
| Column | Type | Behavior |
| ------------ | -------------------- | ---------------------------- |
| `created_at` | `TIMESTAMP(tz=True)` | Auto-set to UTC on creation |
| `updated_at` | `TIMESTAMP(tz=True)` | Auto-updated on modification |
Constants: `FILE_NAME_MAX_LENGTH=450`, `FILE_PATH_MAX_LENGTH=1000`, `FILE_EXTENSION_MAX_LENGTH=100`
### Entity-Relationship Diagram
```text
┌──────────────┐
┌────────>│ client_tokens│
│ └──────────────┘
│ ┌──────────────┐
├────────>│ devices │──────┐
│ └──────────────┘ │
│ v
┌──────────┐ │ ┌──────────────┐ ┌──────────────────┐
│ users │──────────────┼────────>│ saves │<───│ device_save_sync │
└──────────┘ │ └──────────────┘ └──────────────────┘
│ │
│ │ ┌──────────────┐
│ ├────────>│ states │
│ │ └──────────────┘
│ │
│ │ ┌──────────────┐
│ ├────────>│ screenshots │
│ │ └──────────────┘
│ │
│ │ ┌──────────────┐
│ ├────────>│ rom_user │
│ │ └──────────────┘
│ │ │
│ │ │
│ │ ┌──────────────┐ ┌──────────────┐
│ │ ┌───>│ roms │<─────│ platforms │
│ │ │ └──────────────┘ └──────────────┘
│ │ │ │ │
│ │ │ ├────> rom_files ├────> firmware
│ │ │ ├────> roms_metadata │
│ │ │ ├────> rom_notes │
│ │ │ ├────> saves │
│ │ │ ├────> states │
│ │ │ ├────> screenshots │
│ │ │ └────> sibling_roms (self M:M)
│ │ │
│ ┌──────────┴────┴──┐
└────────>│ collections │ (M:M via collections_roms)
├──────────────────┤
│ smart_collections│ (filter-based, dynamic)
├──────────────────┤
│virtual_collections│ (DB view, read-only)
└──────────────────┘
```
### Model Definitions
#### Users
**Table:** `users`
| Column | Type | Notes |
| ----------------- | --------------------------------- | -------------------------- |
| `id` | Integer | PK, autoincrement |
| `username` | String(255) | Unique, indexed |
| `hashed_password` | String(255) | Nullable (OIDC users) |
| `email` | String(255) | Unique, indexed, nullable |
| `enabled` | Boolean | Default `True` |
| `role` | Enum(`VIEWER`, `EDITOR`, `ADMIN`) | Default `VIEWER` |
| `avatar_path` | String(255) | Default `""` |
| `last_login` | Timestamp | Nullable |
| `last_active` | Timestamp | Nullable |
| `ra_username` | String(255) | RetroAchievements username |
| `ra_progression` | JSON | RetroAchievements data |
| `ui_settings` | JSON | User preferences |
**Relationships:** saves (1:M), states (1:M), screenshots (1:M), rom_users (1:M), notes (1:M), collections (1:M), smart_collections (1:M), devices (1:M, cascade), client_tokens (1:M, cascade)
---
#### Platforms
**Table:** `platforms`
| Column | Type | Notes |
| ------------------------------------------------------------------------------------------------------------ | ------------ | ----------------------------- |
| `id` | Integer | PK |
| `slug` | String(100) | Indexed, canonical identifier |
| `fs_slug` | String(100) | Filesystem folder name |
| `name` | String(400) | Display name |
| `custom_name` | String(400) | User override |
| `igdb_id`, `sgdb_id`, `moby_id`, `ss_id`, `ra_id`, `launchbox_id`, `hasheous_id`, `tgdb_id`, `flashpoint_id` | Integer | External provider IDs |
| `category` | String(100) | Platform category |
| `generation` | Integer | Console generation |
| `family_name` / `family_slug` | String(1000) | Platform family |
| `aspect_ratio` | String(10) | Default `"2 / 3"` |
| `missing_from_fs` | Boolean | Default `False` |
**Computed properties:** `rom_count` (subquery), `fs_size_bytes` (sum of ROM sizes)
**Relationships:** roms (1:M), firmware (1:M)
---
#### ROMs
**Table:** `roms` (the central entity)
| Column Group | Columns | Notes |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
| **Identity** | `id`, `platform_id` (FK) | Core identifiers |
| **External IDs** | `igdb_id`, `sgdb_id`, `moby_id`, `ss_id`, `ra_id`, `launchbox_id`, `hasheous_id`, `tgdb_id`, `flashpoint_id`, `hltb_id`, `gamelist_id` | All indexed |
| **Filesystem** | `fs_name`, `fs_name_no_tags`, `fs_name_no_ext`, `fs_extension`, `fs_path`, `fs_size_bytes` | File info |
| **Display** | `name`, `slug`, `summary` | Game metadata |
| **Provider metadata** | `igdb_metadata`, `moby_metadata`, `ss_metadata`, `ra_metadata`, `launchbox_metadata`, `hasheous_metadata`, `flashpoint_metadata`, `hltb_metadata`, `gamelist_metadata`, `manual_metadata` | JSON blobs per provider |
| **Media** | `path_cover_s`, `path_cover_l`, `url_cover`, `path_manual`, `url_manual`, `path_screenshots`, `url_screenshots` | Cover art & screenshots |
| **Classification** | `revision`, `version`, `regions`, `languages`, `tags` | Game attributes |
| **Hashes** | `crc_hash`, `md5_hash`, `sha1_hash`, `ra_hash` | File integrity |
| **State** | `missing_from_fs` | Filesystem sync |
**Relationships:** platform (M:1), files (1:M), saves (1:M), states (1:M), screenshots (1:M), rom_users (1:M), notes (1:M), metadatum (1:1), sibling_roms (M:M self-referential), collections (M:M)
---
#### ROM Files (table)
**Table:** `rom_files`
Tracks individual files within a ROM (archives can contain multiple files).
| Column | Type | Notes |
| ---------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------ |
| `id` | Integer | PK |
| `rom_id` | Integer | FK → roms |
| `file_name`, `file_path` | String | File identity |
| `file_size_bytes` | BigInteger | Size |
| `crc_hash`, `md5_hash`, `sha1_hash`, `ra_hash` | String(100) | Hashes |
| `category` | Enum | `GAME`, `DLC`, `HACK`, `MANUAL`, `PATCH`, `UPDATE`, `MOD`, `DEMO`, `TRANSLATION`, `PROTOTYPE`, `CHEAT` |
| `missing_from_fs` | Boolean | Sync state |
---
#### ROM Metadata (Aggregated)
**Table:** `roms_metadata` (aggregated metadata from all providers)
| Column | Type |
| -------------------- | --------------------------- |
| `rom_id` | Integer (PK, FK → roms) |
| `genres` | JSON |
| `franchises` | JSON |
| `collections` | JSON |
| `companies` | JSON |
| `game_modes` | JSON |
| `age_ratings` | JSON |
| `player_count` | String(100) |
| `first_release_date` | BigInteger (UNIX timestamp) |
| `average_rating` | Float |
---
#### ROM User Data
**Table:** `rom_user` (per-user, per-ROM tracking)
| Column | Type | Notes |
| -------------------- | ------------ | --------------------------------------------------------------------- |
| `rom_id` + `user_id` | FK composite | Unique constraint |
| `is_main_sibling` | Boolean | Primary version flag |
| `last_played` | Timestamp | |
| `backlogged` | Boolean | |
| `now_playing` | Boolean | |
| `hidden` | Boolean | |
| `rating` | Integer | 0-5 |
| `difficulty` | Integer | |
| `completion` | Integer | Percentage |
| `status` | Enum | `INCOMPLETE`, `FINISHED`, `COMPLETED_100`, `RETIRED`, `NEVER_PLAYING` |
---
#### ROM Notes
**Table:** `rom_notes`
| Column | Type | Notes |
| ------------------------------ | ----------- | ----------------- |
| `id` | Integer | PK |
| `rom_id` + `user_id` + `title` | | Unique constraint |
| `title` | String(400) | |
| `content` | Text | |
| `is_public` | Boolean | Default `False` |
| `tags` | JSON | |
---
#### Collections
**Table:** `collections` (manually curated ROM lists)
| Column | Type | Notes |
| ----------------------------- | ----------- | --------- |
| `id` | Integer | PK |
| `user_id` | FK → users | |
| `name` | String(400) | |
| `description` | Text | |
| `is_public` | Boolean | |
| `is_favorite` | Boolean | |
| `path_cover_s/l`, `url_cover` | Text | Cover art |
Linked to ROMs via `collections_roms` join table (M:M).
**Table:** `smart_collections` (dynamic, filter-based)
| Column | Type | Notes |
| ----------------- | ------- | ----------------------- |
| `filter_criteria` | JSON | Query definition |
| `rom_ids` | JSON | Cached matching ROM IDs |
| `rom_count` | Integer | Cached count |
**View:** `virtual_collections` (database view, read-only, excluded from migrations).
---
#### Assets (Saves, States, Screenshots)
All three share a similar structure:
| Table | Extra Columns | Notes |
| ------------- | ---------------------------------- | ------------------- |
| `saves` | `emulator`, `slot`, `content_hash` | Device sync support |
| `states` | `emulator` | Save states |
| `screenshots` | | In-game captures |
Common columns: `id`, `rom_id` (FK), `user_id` (FK), `file_name`, `file_path`, `file_size_bytes`, `missing_from_fs`
Saves additionally link to `device_save_sync` for cross-device tracking.
---
#### Devices & Sync
**Table:** `devices`
| Column | Type | Notes |
| ---------------------------------------------- | ----------- | ----------------------------------- |
| `id` | String(255) | UUID, PK |
| `user_id` | FK → users | |
| `name`, `platform`, `client`, `client_version` | String | Device info |
| `sync_mode` | Enum | `API`, `FILE_TRANSFER`, `PUSH_PULL` |
| `sync_enabled` | Boolean | |
| `last_seen` | Timestamp | |
**Table:** `device_save_sync` (tracks per-device, per-save sync state)
| Column | Type |
| ----------------------- | ------------ |
| `device_id` + `save_id` | Composite PK |
| `last_synced_at` | Timestamp |
| `is_untracked` | Boolean |
---
#### Client Tokens
**Table:** `client_tokens` (long-lived API tokens)
| Column | Type | Notes |
| -------------- | ------------ | ---------------------------- |
| `id` | Integer | PK |
| `user_id` | FK → users | |
| `name` | String(255) | Display name |
| `hashed_token` | String(64) | SHA-256 hash, unique |
| `scopes` | String(1000) | Space-separated OAuth scopes |
| `expires_at` | Timestamp | Nullable |
| `last_used_at` | Timestamp | |
Token format: `rmm_` + 64 hex chars (32-byte random)
---
#### Firmware
**Table:** `firmware`
| Column | Type | Notes |
| ----------------------------------- | -------------- | ----------------------------- |
| `id` | Integer | PK |
| `platform_id` | FK → platforms | |
| `file_name`, `file_path` | String | |
| `crc_hash`, `md5_hash`, `sha1_hash` | String | Integrity |
| `is_verified` | Boolean | Matches known_bios_files.json |
---
### Alembic Migrations
80+ migration scripts in `alembic/versions/`. Key milestones:
| Migration | Description |
| -------------- | ----------------------------------------- |
| `0009` | Models refactor |
| `0014`, `0019` | Asset filesystem refactoring |
| `0020` | Added created_at/updated_at to all tables |
| `0021` | ROM user associations |
| `0022` | Collection system |
| `0023` | Column nullability constraints |
| `0024` | Sibling ROM database views |
| `0025` | ROM hash tracking |
| `0064` | Performance indexes on updated_at |
| `0068` | Device + device_save_sync tables |
| `0072` | Client tokens table |
Migrations support batch mode for SQLite and DB-specific SQL for MariaDB/MySQL/PostgreSQL.
---
## 6. API Endpoints
**Base URL:** `/api`
**Documentation:** Swagger UI at `/api/docs`, ReDoc at `/api/redoc`
**Pagination:** `fastapi-pagination` with `LimitOffsetParams` (`limit`, `offset`, `total`)
### 6.1 Authentication (`/api`)
| Method | Path | Auth | Description |
| ------ | ------------------ | ---- | ---------------------------------------------- |
| POST | `/login` | No | Session login (HTTP Basic) |
| POST | `/logout` | No | Logout (returns OIDC logout URL if configured) |
| POST | `/token` | No | OAuth2 token (password, refresh_token grants) |
| GET | `/login/openid` | No | OIDC login redirect |
| GET | `/oauth/openid` | No | OIDC callback |
| POST | `/forgot-password` | No | Request password reset |
| POST | `/reset-password` | No | Reset password with token |
### 6.2 Users (`/api/users`)
| Method | Path | Scope | Description |
| ------ | ------------------ | ---------------------- | -------------------------------- |
| POST | `/` | ME_WRITE / USERS_WRITE | Create user (first user = admin) |
| POST | `/invite-link` | USERS_WRITE | Generate invite token |
| POST | `/register` | None | Register with invite token |
| GET | `/` | USERS_READ | List all users |
| GET | `/identifiers` | USERS_READ | Get user IDs |
| GET | `/me` | ME_READ | Current user profile |
| GET | `/{id}` | USERS_READ | Get user by ID |
| PUT | `/{id}` | ME_WRITE | Update user |
| DELETE | `/{id}` | USERS_WRITE | Delete user |
| POST | `/{id}/ra/refresh` | ME_WRITE | Refresh RetroAchievements data |
### 6.3 Client Tokens (`/api/client-tokens`)
| Method | Path | Scope | Description |
| ------ | --------------------- | ----------- | ---------------------------- |
| POST | `/` | ME_WRITE | Create token |
| GET | `/` | ME_READ | List user's tokens |
| DELETE | `/{id}` | ME_WRITE | Delete token |
| PUT | `/{id}/regenerate` | ME_WRITE | Regenerate token |
| POST | `/{id}/pair` | ME_WRITE | Generate pair code |
| GET | `/pair/{code}/status` | None | Check pair status |
| POST | `/exchange` | None | Exchange pair code for token |
| GET | `/all` | USERS_READ | Admin: list all tokens |
| DELETE | `/{id}/admin` | USERS_WRITE | Admin: delete any token |
### 6.4 Platforms (`/api/platforms`)
| Method | Path | Scope | Description |
| ------ | -------------- | --------------- | -------------------------------------------- |
| POST | `/` | PLATFORMS_WRITE | Create platform |
| GET | `/` | PLATFORMS_READ | List platforms (with `updated_after` filter) |
| GET | `/identifiers` | PLATFORMS_READ | Get platform IDs |
| GET | `/supported` | PLATFORMS_READ | List supported platforms |
| GET | `/{id}` | PLATFORMS_READ | Get platform |
| PUT | `/{id}` | PLATFORMS_WRITE | Update platform |
| DELETE | `/{id}` | PLATFORMS_WRITE | Delete platform |
### 6.5 ROMs (`/api/roms`)
| Method | Path | Scope | Description |
| ------ | ---------------------------- | ---------- | --------------------------------- |
| GET | `/` | ROMS_READ | List ROMs (paginated, filterable) |
| GET | `/identifiers` | ROMS_READ | Get ROM IDs |
| GET | `/{id}` | ROMS_READ | Get ROM details |
| PUT | `/{id}` | ROMS_WRITE | Update ROM metadata |
| PUT | `/{id}/user` | ME_WRITE | Update user-specific ROM data |
| DELETE | `/{id}` | ROMS_WRITE | Delete ROM |
| POST | `/delete` | ROMS_WRITE | Bulk delete |
| POST | `/download/{id}/{file_name}` | ROMS_READ | Download ROM |
| POST | `/unidentified` | ROMS_READ | Get unidentified ROMs |
#### ROM Upload (Chunked)
| Method | Path | Scope | Description |
| ------ | ----------------------- | ---------- | ------------------------- |
| POST | `/upload/init` | ROMS_WRITE | Initialize upload session |
| POST | `/upload/{id}/chunk` | ROMS_WRITE | Upload chunk (max 64MB) |
| POST | `/upload/{id}/complete` | ROMS_WRITE | Finalize upload |
| GET | `/upload/{id}/session` | ROMS_WRITE | Check session status |
| DELETE | `/upload/{id}` | ROMS_WRITE | Cancel upload |
#### ROM Files
| Method | Path | Scope | Description |
| ------ | ---------------------------- | --------- | --------------------------------------- |
| GET | `/{id}/files` | ROMS_READ | Get ROM file metadata |
| GET | `/{id}/files/content/{name}` | ROMS_READ | Download file (nginx X-Accel or direct) |
### 6.6 Search (`/api/search`)
| Method | Path | Scope | Description |
| ------ | -------- | --------- | ------------------------------------ |
| GET | `/roms` | ROMS_READ | Search metadata across all providers |
| GET | `/cover` | ROMS_READ | Search SteamGridDB for cover art |
### 6.7 Saves (`/api/saves`)
| Method | Path | Scope | Description |
| ------ | ------------------ | ------------- | --------------------------------------- |
| POST | `/` | ASSETS_WRITE | Upload save (with optional device sync) |
| GET | `/` | ASSETS_READ | List saves (with device_id filter) |
| GET | `/identifiers` | ASSETS_READ | Get save IDs |
| GET | `/summary` | ASSETS_READ | Saves grouped by slot |
| GET | `/{id}` | ASSETS_READ | Get save |
| GET | `/{id}/content` | ASSETS_READ | Download save file |
| POST | `/{id}/downloaded` | DEVICES_WRITE | Confirm download (device sync) |
| PUT | `/{id}` | ASSETS_WRITE | Update save |
| POST | `/delete` | ASSETS_WRITE | Bulk delete |
| POST | `/{id}/track` | DEVICES_WRITE | Re-enable sync tracking |
| POST | `/{id}/untrack` | DEVICES_WRITE | Disable sync tracking |
### 6.8 States (`/api/states`)
| Method | Path | Scope | Description |
| ------ | -------------- | ------------ | ------------- |
| POST | `/` | ASSETS_WRITE | Upload state |
| GET | `/` | ASSETS_READ | List states |
| GET | `/identifiers` | ASSETS_READ | Get state IDs |
| GET | `/{id}` | ASSETS_READ | Get state |
| PUT | `/{id}` | ASSETS_WRITE | Update state |
| POST | `/delete` | ASSETS_WRITE | Bulk delete |
### 6.9 Screenshots (`/api/screenshots`)
| Method | Path | Scope | Description |
| ------ | -------------- | ------------ | ------------------ |
| POST | `/` | ASSETS_WRITE | Upload screenshot |
| GET | `/` | ASSETS_READ | List screenshots |
| GET | `/identifiers` | ASSETS_READ | Get screenshot IDs |
| GET | `/{id}` | ASSETS_READ | Get screenshot |
| PUT | `/{id}` | ASSETS_WRITE | Update screenshot |
| POST | `/delete` | ASSETS_WRITE | Bulk delete |
### 6.10 Devices (`/api/devices`)
| Method | Path | Scope | Description |
| ------ | ------- | ------------- | ----------------------------------- |
| POST | `/` | DEVICES_WRITE | Register device (fingerprint dedup) |
| GET | `/` | DEVICES_READ | List devices |
| GET | `/{id}` | DEVICES_READ | Get device |
| PUT | `/{id}` | DEVICES_WRITE | Update device |
| DELETE | `/{id}` | DEVICES_WRITE | Delete device |
### 6.11 Collections (`/api/collections`)
| Method | Path | Scope | Description |
| ------ | --------------------- | ----------------- | --------------------- |
| POST | `/` | COLLECTIONS_WRITE | Create collection |
| GET | `/` | COLLECTIONS_READ | List collections |
| GET | `/identifiers` | COLLECTIONS_READ | Get collection IDs |
| GET | `/{id}` | COLLECTIONS_READ | Get collection |
| PUT | `/{id}` | COLLECTIONS_WRITE | Update collection |
| DELETE | `/{id}` | COLLECTIONS_WRITE | Delete collection |
| POST | `/{id}/roms` | COLLECTIONS_WRITE | Add ROM to collection |
| DELETE | `/{id}/roms/{rom_id}` | COLLECTIONS_WRITE | Remove ROM |
### 6.12 Feeds (`/api/feeds`)
| Method | Path | Description |
| ------ | --------------------- | ----------------------------- |
| GET | `/webrcade` | WebRcade feed format |
| GET | `/tinfoil` | Tinfoil custom index (Switch) |
| GET | `/pkgi/ps3/{type}` | PKGi PS3 database |
| GET | `/pkgi/psvita/{type}` | PKGi PS Vita database |
| GET | `/pkgi/psp/{type}` | PKGi PSP database |
| GET | `/fpkgi/{platform}` | FPKGi (PS4/PS5) format |
| GET | `/kekatsu/{platform}` | Kekatsu DS format |
| GET | `/pkgj/psp/games` | PKGj PSP games |
| GET | `/pkgj/psp/dlc` | PKGj PSP DLC |
| GET | `/pkgj/psvita/games` | PKGj PS Vita games |
| GET | `/pkgj/psvita/dlc` | PKGj PS Vita DLC |
| GET | `/pkgj/psx/games` | PKGj PSX games |
### 6.13 Configuration (`/api/config`)
| Method | Path | Scope | Description |
| ------ | -------------------------- | --------------- | ------------------------ |
| GET | `/` | None | Get RomM configuration |
| POST | `/system/platforms` | PLATFORMS_WRITE | Add platform binding |
| DELETE | `/system/platforms/{slug}` | PLATFORMS_WRITE | Remove platform binding |
| POST | `/system/versions` | PLATFORMS_WRITE | Add version mapping |
| DELETE | `/system/versions/{slug}` | PLATFORMS_WRITE | Remove version mapping |
| POST | `/system/exclusions` | PLATFORMS_WRITE | Add exclusion pattern |
| DELETE | `/system/exclusions` | PLATFORMS_WRITE | Remove exclusion pattern |
### 6.14 Tasks (`/api/tasks`)
| Method | Path | Scope | Description |
| ------ | ------------- | --------- | ------------------------ |
| GET | `/` | TASKS_RUN | List all available tasks |
| GET | `/status` | TASKS_RUN | Status of all tasks |
| GET | `/{id}` | TASKS_RUN | Status of specific task |
| POST | `/run/{name}` | TASKS_RUN | Trigger task execution |
### 6.15 Other Endpoints
| Router | Path | Description |
| ------------- | -------------------------------------- | -------------------------------------- |
| Heartbeat | `GET /api/heartbeat` | System info, version, metadata sources |
| Heartbeat | `GET /api/heartbeat/metadata/{source}` | Check metadata provider health |
| Heartbeat | `GET /api/setup/library` | Library structure info (wizard) |
| Heartbeat | `POST /api/setup/platforms` | Create platform folders (wizard) |
| Stats | `GET /api/stats` | Library statistics |
| Raw | `HEAD /api/raw/assets/{path}` | Check asset existence |
| Raw | `GET /api/raw/assets/{path}` | Serve raw asset file |
| Firmware | Standard CRUD | BIOS file management |
| Export | `POST /api/export/gamelist-xml` | Export ES-DE gamelist.xml |
| Export | `POST /api/export/pegasus` | Export Pegasus frontend metadata |
| Netplay | `GET /api/netplay/list` | List netplay rooms |
| Play Sessions | `POST /api/play-sessions` | Ingest play session from client |
| Play Sessions | `GET /api/play-sessions` | List play sessions (per user / ROM) |
| Sync | `/api/sync/*` | Device sync session coordination |
The codebase exposes roughly **175 HTTP routes** and **11 WebSocket handlers** across 24 routers.
---
## 7. Authentication & Authorization
### Authentication Methods
```text
┌──────────────────────────────────────────────────────────────┐
│ HybridAuthBackend │
│ │
│ 1. Check session cookie (romm_session) │
│ └─ Redis lookup → user from session["sub"] │
│ │
│ 2. Check Authorization header │
│ ├─ "Basic ..." → bcrypt password verify │
│ ├─ "Bearer ..." → JWT validation (HS256) │
│ └─ "Bearer rmm_..." → Client API token (SHA-256 lookup) │
│ │
│ 3. OIDC (if enabled) │
│ └─ Token from OIDC provider → email match → user │
│ │
│ 4. Kiosk mode (if enabled) │
│ └─ Anonymous access with read-only scopes │
│ │
│ Falls through all methods → 401 Unauthorized │
└──────────────────────────────────────────────────────────────┘
```
### Token Types
| Token | Format | Lifetime | Storage |
| ---------------- | --------------------- | ---------------------- | ----------------------- |
| Access Token | JWT (HS256) | 30 min (configurable) | Client-side |
| Refresh Token | JWT with JTI | 7 days (configurable) | JTI in Redis |
| Session | Cookie `romm_session` | 14 days (configurable) | Redis |
| Client API Token | `rmm_` + 64 hex chars | Configurable / never | SHA-256 hash in DB |
| CSRF Token | Signed cookie | Session lifetime | Cookie + header |
| Password Reset | JWT with JTI | 10 minutes | JTI in Redis (one-time) |
| Invite Link | JWT with JTI | 10 minutes | JTI in Redis (one-time) |
### Role-Based Access Control
| Role | Scopes | Description |
| -------- | ---------------------------------------- | --------------- |
| `VIEWER` | Read all + write own profile | Default role |
| `EDITOR` | VIEWER + write ROMs, platforms, assets | Content manager |
| `ADMIN` | EDITOR + user management, task execution | Full access |
### Scope Definitions
```text
me.read / me.write : Own profile
roms.read / roms.write : ROM data
platforms.read / platforms.write : Platform data
assets.read / assets.write : Saves, states, screenshots
devices.read / devices.write : Device management
firmware.read / firmware.write : BIOS files
collections.read / collections.write : Collections
users.read / users.write : User management (admin)
tasks.run : Task execution
```
### CSRF Protection
- Cookie: `romm_csrftoken` (signed with `itsdangerous`)
- Header: `x-csrftoken`
- Both must match and contain the authenticated user's ID
- Exempt: `/api/token`, `/api/client-tokens/exchange`, `/api/client-tokens/pair/*/status`, `/ws`, `/netplay`
- Skipped for requests with `Authorization: Bearer` or `Authorization: Basic` headers
### Session Management
- Redis keys: `session:{session_id}`, `user_sessions:{username}`
- Cookie: `romm_session` (httponly, samesite=lax/strict)
- `clear_user_sessions(user_id)` on password change clears all sessions
---
## 8. Business Logic (Handlers)
### 8.1 Scan Handler (`handler/scan_handler.py`)
The core of RomM. Orchestrates library scanning and metadata enrichment.
**Scan Types:**
| Type | Behavior |
| --------------- | -------------------------------- |
| `NEW_PLATFORMS` | Detect new platform folders only |
| `QUICK` | Scan new/unscanned ROMs |
| `UPDATE` | Rescan already-identified ROMs |
| `UNMATCHED` | Rescan ROMs without metadata |
| `COMPLETE` | Full rescan of everything |
| `HASHES` | Recalculate all file hashes |
**Scan Flow:**
```text
1. Detect platform folders in LIBRARY_BASE_PATH
2. For each platform:
├── Map filesystem slug to canonical platform (via config bindings)
├── Query metadata providers for platform info
└── Create/update platform in DB
3. For each ROM file in platform:
├── Parse filename (extract name, tags, region, version)
├── Calculate file hashes (CRC32, MD5, SHA1)
├── Search metadata providers (in priority order):
│ ├── IGDB
│ ├── MobyGames
│ ├── ScreenScraper
│ ├── LaunchBox
│ ├── RetroAchievements
│ ├── Hasheous (hash-based matching)
│ ├── Flashpoint
│ ├── HLTB
│ └── TheGamesDB
├── Download cover art and screenshots
├── Build aggregated metadata (RomMetadata)
└── Create/update ROM in DB
4. Emit real-time progress via Socket.IO
5. Mark missing ROMs (files no longer on filesystem)
```
**Search Term Normalization:**
- Remove articles ("The", "A", "An")
- Strip punctuation and special characters
- Unicode normalization (NFKD)
- Jaro-Winkler similarity matching for fuzzy results
### 8.2 Database Handlers (`handler/database/`)
Each entity has a dedicated handler providing CRUD operations:
| Handler | Key Operations |
| ----------------------------- | ---------------------------------------------------------------------------------- |
| `db_roms_handler` | Advanced filtering (platform, genre, region, status), pagination, file association |
| `db_platform_handler` | Slug mapping, ROM count aggregation |
| `db_users_handler` | Role management, credential storage |
| `db_saves_handler` | Slot-based grouping, device sync tracking |
| `db_collections_handler` | ROM association, smart collection evaluation |
| `db_stats_handler` | Platform/ROM counts, storage usage, metadata coverage |
| `db_devices_handler` | Fingerprint deduplication |
| `db_device_save_sync_handler` | Cross-device sync state |
| `db_client_tokens_handler` | Hash-based lookup, scope management |
| `db_play_sessions_handler` | Play session ingestion and aggregation |
| `db_sync_sessions_handler` | Device sync session lifecycle (push/pull, SSH) |
### 8.3 Metadata Handlers (`handler/metadata/`)
Each external provider has a handler that normalizes data into a common format:
| Handler | Provider | Key Data |
| -------------------- | ----------------- | ------------------------------------------ |
| `igdb_handler` | IGDB | Game info, covers, screenshots, franchises |
| `moby_handler` | MobyGames | Publisher, genre classification |
| `ss_handler` | ScreenScraper | Regional metadata, box art, manuals |
| `sgdb_handler` | SteamGridDB | Grid artwork, logos, icons |
| `ra_handler` | RetroAchievements | Achievements, user progression |
| `hltb_handler` | HowLongToBeat | Playtime estimates |
| `hasheous_handler` | Hasheous | Hash-based ROM identification |
| `tgdb_handler` | TheGamesDB | Alternative metadata |
| `flashpoint_handler` | Flashpoint | Browser game archive |
| `gamelist_handler` | gamelist.xml | ES-DE format parser |
| `libretro_handler` | Libretro | Libretro thumbnails DB |
| `playmatch_handler` | PlayMatch | Game matching algorithm |
| `launchbox_handler/` | LaunchBox | Local + remote database, media |
**Priority system:** Metadata sources are queried in configurable priority order. First match wins for each field, with manual overrides taking highest priority.
### 8.4 Filesystem Handlers (`handler/filesystem/`)
| Handler | Responsibility |
| ------------------- | -------------------------------------------------- |
| `roms_handler` | Read ROM files, calculate hashes, extract archives |
| `assets_handler` | Store/retrieve saves, states, screenshots |
| `firmware_handler` | BIOS file management, verification |
| `platforms_handler` | Platform folder creation and detection |
| `resources_handler` | Download and cache artwork |
**Supported archive formats:** ZIP, 7Z, TAR, GZIP, BZ2, RAR
**Special hash handling:**
- CHD (Compressed Hunks of Data) v5: SHA1 extracted from header
- PICO-8 cartridges (.p8.png): special handling
- RetroAchievements hash (`ra_hash`): platform-specific algorithm via `rahasher.py`
- Non-hashable platforms (Switch, PS3/4/5): hashing skipped
### 8.5 Netplay Handler (`handler/netplay_handler.py`)
Manages real-time multiplayer rooms stored in Redis:
```python
NetplayRoom:
owner: str # User ID
players: dict # sid → NetplayPlayerInfo
peers: list[str] # Peer IDs
room_name: str
game_id: str # ROM ID
domain: Optional[str]
password: Optional[str]
max_players: int
```
### 8.6 Play Sessions
Tracks per-user playtime events ingested from clients (web player, console mode, external launchers) via `POST /api/play-sessions`. Persisted in `play_sessions` table; aggregated into user profile stats and recent-activity feeds.
### 8.7 Device Sync Sessions
Coordinates save/state synchronization between devices using three sync modes (`API`, `FILE_TRANSFER`, `PUSH_PULL`). `SyncSession` tracks the lifecycle of a push/pull operation (including optional SSH-based file transfer; see `SYNC_SSH_*` env vars). Endpoints live in `endpoints/sync.py`; state is stored in the `sync_sessions` table.
### 8.8 Socket Handler (`handler/socket_handler.py`)
Manages two Socket.IO servers:
| Server | Mount | Purpose |
| ------------------------ | ---------- | ------------------------------------ |
| `socket_handler` | `/ws` | Scan progress, general notifications |
| `netplay_socket_handler` | `/netplay` | Netplay room management |
Both use Redis as the message queue backend for horizontal scaling.
**Scan Progress Events:**
```python
ScanStats:
total_platforms, scanned_platforms, new_platforms
total_roms, scanned_roms, new_roms, identified_roms
scanned_firmware, new_firmware
```
---
## 9. External Integrations (Adapters)
**Location:** `adapters/services/`
Each adapter wraps an external API with authentication, retry logic, and type safety.
### IGDB (Internet Game Database)
| Property | Value |
| --------------- | ----------------------------------------------------------------------- |
| **Auth** | Twitch OAuth2 (client_id + client_secret → bearer token) |
| **Data** | Game metadata, covers, screenshots, age ratings, franchises, game modes |
| **Rate limits** | Retry logic with backoff |
| **Config vars** | `IGDB_CLIENT_ID`, `IGDB_CLIENT_SECRET` |
### MobyGames
| Property | Value |
| -------------- | --------------------------------------------------- |
| **Auth** | API key |
| **Data** | Game metadata, publisher info, genre classification |
| **Config var** | `MOBYGAMES_API_KEY` |
### ScreenScraper
| Property | Value |
| --------------- | ------------------------------------------------------------- |
| **Auth** | Device ID + user credentials |
| **Data** | Regional game metadata, box art, screenshots, manuals, bezels |
| **Media types** | Box 2D/3D, screenshot, video, manual, marquee, bezel |
| **Config vars** | `SCREENSCRAPER_USER`, `SCREENSCRAPER_PASSWORD` |
### SteamGridDB
| Property | Value |
| -------------- | -------------------------------------------------------- |
| **Auth** | Bearer token |
| **Data** | Grid artwork, logos, icons in multiple dimensions/styles |
| **Filters** | Style, dimension, MIME type |
| **Config var** | `STEAMGRIDDB_API_KEY` |
### RetroAchievements
| Property | Value |
| -------------- | ------------------------------------------------- |
| **Auth** | API key (query parameter) |
| **Data** | Game achievements, user progression, award badges |
| **Hash** | Platform-specific hash via `rahasher.py` |
| **Config var** | `RETROACHIEVEMENTS_API_KEY` |
### Additional Providers
| Provider | Handler | Description |
| ------------- | -------------------- | ------------------------------------------------- |
| LaunchBox | `launchbox_handler/` | Local XML database + remote API, platform mapping |
| HowLongToBeat | `hltb_handler` | Game playtime estimates |
| Hasheous | `hasheous_handler` | Hash-based ROM identification |
| TheGamesDB | `tgdb_handler` | Alternative game metadata |
| Flashpoint | `flashpoint_handler` | Browser game archive database |
| PlayMatch | `playmatch_handler` | Game matching algorithm |
### Static Fixture Data
Cached in Redis at startup from JSON files:
| Fixture | Location | Purpose |
| ----------------------- | ---------------------------- | ------------------------------ |
| `mame_index.json` | `handler/metadata/fixtures/` | MAME ROM name → game info |
| `scummvm_index.json` | `handler/metadata/fixtures/` | ScummVM game identification |
| `ps1_serial_index.json` | `handler/metadata/fixtures/` | PS1 serial code → game mapping |
| `ps2_serial_index.json` | `handler/metadata/fixtures/` | PS2 serial code → game mapping |
| `ps2_opl_index.json` | `handler/metadata/fixtures/` | PS2 OPL serial codes |
| `psp_serial_index.json` | `handler/metadata/fixtures/` | PSP serial code → game mapping |
| `known_bios_files.json` | `models/fixtures/` | Verified BIOS file hashes |
---
## 10. Real-Time Communication (WebSockets)
### Socket Architecture
```text
Client ←──Socket.IO──→ FastAPI (python-socketio) ←──Redis PubSub──→ Workers
```
### Scan Progress (`/ws`)
**Events emitted to clients:**
| Event | Payload | When |
| ------------------- | ------------------ | --------------------------- |
| `scan:update_stats` | `ScanStats` object | Each ROM/platform processed |
| `scan:log` | Log message | Scan log entries |
| `scan:stop` | | Scan completed or cancelled |
### Netplay (`/netplay`)
**Events:**
| Event | Direction | Description |
| --------------- | --------------- | ----------------------- |
| `open-room` | Client → Server | Create netplay room |
| `join-room` | Client → Server | Join existing room |
| `users-updated` | Server → Client | Player list changed |
| Message relay | Bidirectional | Game data between peers |
Redis-backed for horizontal scaling across multiple server instances.
---
## 11. Background Tasks & Scheduling
### Job Queue System
**Technology:** RQ (Redis Queue)
**Priority Queues:**
| Queue | Use Case |
| ----------------- | --------------------------- |
| `high_prio_queue` | Urgent operations |
| `default_queue` | Standard background work |
| `low_prio_queue` | Long-running scans, cleanup |
### Scheduled Tasks
Configured via environment variables and managed by RQ Scheduler:
| Task | Env Toggle | Default Cron | Description |
| --------------------------------- | -------------------------------------------------- | ------------------ | ---------------------- |
| `scan_library` | `ENABLE_SCHEDULED_RESCAN` | `0 3 * * *` (3 AM) | Full library rescan |
| `update_switch_titledb` | `ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB` | `0 4 * * *` | Update Switch game DB |
| `update_launchbox_metadata` | `ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA` | `0 4 * * *` | Refresh LaunchBox data |
| `convert_images_to_webp` | `ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP` | `0 4 * * *` | Image optimization |
| `sync_retroachievements_progress` | `ENABLE_SCHEDULED_RETROACHIEVEMENTS_PROGRESS_SYNC` | `0 4 * * *` | Sync RA user progress |
| `cleanup_netplay` | Always enabled | Periodic | Clean stale rooms |
### Manual Tasks
Triggered via `POST /api/tasks/run/{task_name}`:
| Task | Description |
| ---------------------------- | --------------------------------------------- |
| `cleanup_missing_roms` | Remove DB entries for files no longer on disk |
| `cleanup_orphaned_resources` | Remove unused artwork/resource files |
| `sync_folder_scan` | Scan sync folder for new device saves |
### Filesystem Watcher
**File:** `watcher.py`
Uses `watchfiles` to monitor the library directory for changes. When enabled (`ENABLE_RESCAN_ON_FILESYSTEM_CHANGE`), triggers a rescan after a configurable delay (`RESCAN_ON_FILESYSTEM_CHANGE_DELAY`, default 5 minutes).
---
## 12. File System Management
### Directory Layout
```text
{ROMM_BASE_PATH}/ # Default: /romm
├── library/ # ROM files, organized by platform
│ ├── n64/
│ │ └── roms/
│ │ ├── Game1.z64
│ │ └── Game2.z64
│ ├── psx/
│ │ └── roms/
│ │ └── Game.bin
│ └── {platform_slug}/
│ ├── roms/ # Configurable folder name
│ └── bios/ # Firmware/BIOS files
├── resources/ # Cached metadata assets
│ └── roms/
│ └── {rom_id}/
│ ├── cover_s.webp # Small cover
│ ├── cover_l.webp # Large cover
│ └── screenshots/
├── assets/ # User-generated assets
│ └── users/
│ └── {user_id}/
│ └── {rom_id}/
│ ├── saves/
│ ├── states/
│ └── screenshots/
└── config/
└── config.yml # YAML configuration
```
### File Serving
| Mode | Mechanism | When |
| ----------- | ------------------------ | --------------- |
| Development | `FileResponse` (direct) | `DEV_MODE=true` |
| Production | Nginx `X-Accel-Redirect` | Default |
Nginx receives an internal redirect header and efficiently serves the file from disk without passing bytes through the Python process.
### Hash Calculation
| Algorithm | Used For |
| --------- | -------------------------------------------------------------- |
| CRC32 | Quick integrity check, standard ROM identification |
| MD5 | Content deduplication (saves), BIOS verification |
| SHA1 | ROM identification, BIOS verification |
| RA Hash | RetroAchievements-specific hash (platform-dependent algorithm) |
Hashing can be disabled per-installation via `skip_hash_calculation` in config.yml.
---
## 13. Caching (Redis)
### Cache Architecture
Two Redis client instances:
| Client | Type | Purpose |
| -------------- | ----------------------- | ------------------------------ |
| `redis_client` | Sync (with auto-decode) | Cache queries, session lookups |
| `async_cache` | Async | Async operations |
Falls back to `FakeRedis` in test mode.
### Cache Key Patterns
| Pattern | TTL | Content |
| -------------------------- | --------------- | ------------------------------- |
| `session:{id}` | 14 days | Session JSON |
| `user_sessions:{username}` | 14 days | Set of session IDs |
| `reset-jti:{jti}` | 10 min | Password reset token (one-time) |
| `invite-jti:{jti}` | 10 min | Invite token (one-time) |
| `refresh-jti:{jti}` | 7 days | Refresh token validation |
| `romm:mame_index` | Permanent | MAME game index |
| `romm:scummvm_index` | Permanent | ScummVM game index |
| `romm:ps1_serials` | Permanent | PS1 serial codes |
| `romm:ps2_serials` | Permanent | PS2 serial codes |
| `romm:psp_serials` | Permanent | PSP serial codes |
| `romm:switch_titledb` | Refreshed daily | Switch TitleDB |
| `romm:known_bios` | Permanent | Verified BIOS hashes |
| Upload sessions | 24 hours | Chunked upload state |
| Netplay rooms | Dynamic | Active room state |
---
## 14. Configuration
### Environment Variables
#### Core
| Variable | Default | Description |
| ---------------- | ---------------- | -------------------- |
| `ROMM_BASE_PATH` | `/romm` | Base data directory |
| `ROMM_BASE_URL` | `http://0.0.0.0` | Application base URL |
| `ROMM_PORT` | `8080` | Server port |
| `DEV_MODE` | `false` | Development mode |
| `LOGLEVEL` | `INFO` | Log level |
#### Database
| Variable | Default | Description |
| ---------------- | --------- | ----------------------------------- |
| `ROMM_DB_DRIVER` | `mariadb` | `mariadb`, `mysql`, or `postgresql` |
| `DB_HOST` | | Database host |
| `DB_PORT` | `3306` | Database port |
| `DB_USER` | | Database user |
| `DB_PASSWD` | | Database password |
| `DB_NAME` | `romm` | Database name |
#### Redis
| Variable | Default | Description |
| ---------------- | ----------- | --------------------- |
| `REDIS_HOST` | `127.0.0.1` | Redis host |
| `REDIS_PORT` | `6379` | Redis port |
| `REDIS_USERNAME` | | Redis username (ACL) |
| `REDIS_PASSWORD` | | Redis password |
| `REDIS_DB` | `0` | Redis database number |
| `REDIS_SSL` | `false` | Enable SSL |
#### Authentication
| Variable | Default | Description |
| ------------------------------------ | --------- | ------------------------------------- |
| `ROMM_AUTH_SECRET_KEY` | | **Required.** JWT/session signing key |
| `OAUTH_ACCESS_TOKEN_EXPIRE_SECONDS` | `1800` | 30 minutes |
| `OAUTH_REFRESH_TOKEN_EXPIRE_SECONDS` | `604800` | 7 days |
| `SESSION_MAX_AGE_SECONDS` | `1209600` | 14 days |
| `DISABLE_CSRF_PROTECTION` | `false` | Disable CSRF |
| `DISABLE_DOWNLOAD_ENDPOINT_AUTH` | `false` | Allow unauthenticated downloads |
| `DISABLE_USERPASS_LOGIN` | `false` | Disable password login |
| `DISABLE_LOGS_VIEWER` | `false` | Disable backend log viewer + endpoint |
| `KIOSK_MODE` | `false` | Read-only anonymous access |
#### OIDC
| Variable | Default | Description |
| ------------------------------- | -------------------- | ------------------------------- |
| `OIDC_ENABLED` | `false` | Enable OpenID Connect |
| `OIDC_PROVIDER` | | Provider URL |
| `OIDC_CLIENT_ID` | | Client ID |
| `OIDC_CLIENT_SECRET` | | Client secret |
| `OIDC_REDIRECT_URI` | | Redirect URI |
| `OIDC_USERNAME_ATTRIBUTE` | `preferred_username` | Username claim |
| `OIDC_CLAIM_ROLES` | | Roles claim name |
| `OIDC_ROLE_VIEWER/EDITOR/ADMIN` | | Role mappings |
| `OIDC_TLS_CACERTFILE` | | Custom CA bundle for OIDC calls |
| `OIDC_RP_INITIATED_LOGOUT` | `false` | Send logout to OIDC provider |
| `OIDC_END_SESSION_ENDPOINT` | | End-session URL override |
#### API Keys
| Variable | Description |
| ----------------------------------------------- | ----------------- |
| `IGDB_CLIENT_ID` + `IGDB_CLIENT_SECRET` | IGDB (via Twitch) |
| `MOBYGAMES_API_KEY` | MobyGames |
| `SCREENSCRAPER_USER` + `SCREENSCRAPER_PASSWORD` | ScreenScraper |
| `STEAMGRIDDB_API_KEY` | SteamGridDB |
| `RETROACHIEVEMENTS_API_KEY` | RetroAchievements |
#### Feature Toggles
| Variable | Default | Description |
| ------------------------ | ------- | ------------------------ |
| `LAUNCHBOX_API_ENABLED` | `false` | LaunchBox metadata |
| `PLAYMATCH_API_ENABLED` | `false` | PlayMatch matching |
| `HASHEOUS_API_ENABLED` | `false` | Hasheous identification |
| `TGDB_API_ENABLED` | `false` | TheGamesDB |
| `FLASHPOINT_API_ENABLED` | `false` | Flashpoint archive |
| `HLTB_API_ENABLED` | `false` | HowLongToBeat |
| `DISABLE_EMULATOR_JS` | `false` | Hide EmulatorJS player |
| `DISABLE_RUFFLE_RS` | `false` | Hide Ruffle Flash player |
#### Task Scheduling
| Variable | Default | Description |
| -------------------------------------- | ----------- | ------------------------------- |
| `SCAN_TIMEOUT` | `14400` | 4-hour scan timeout |
| `SCAN_WORKERS` | `1` | Concurrent scan workers |
| `TASK_TIMEOUT` | | RQ job timeout for manual tasks |
| `TASK_RESULT_TTL` | | How long to keep job results |
| `ENABLE_SCHEDULED_RESCAN` | `false` | Auto library rescan |
| `SCHEDULED_RESCAN_CRON` | `0 3 * * *` | Rescan schedule |
| `ENABLE_RESCAN_ON_FILESYSTEM_CHANGE` | `false` | Watch for file changes |
| `RESCAN_ON_FILESYSTEM_CHANGE_DELAY` | `5` | Debounce delay (minutes) |
| `SEVEN_ZIP_TIMEOUT` | | Timeout for 7-Zip extraction |
| `REFRESH_RETROACHIEVEMENTS_CACHE_DAYS` | | RA cache TTL (days) |
#### Device Sync
| Variable | Default | Description |
| ---------------------------- | ------- | ------------------------------- |
| `ENABLE_SYNC_FOLDER_WATCHER` | `false` | Watch sync folder for new saves |
| `SYNC_FOLDER_SCAN_DELAY` | | Debounce for sync folder scans |
| `ENABLE_SYNC_PUSH_PULL` | `false` | Enable scheduled push/pull sync |
| `SYNC_PUSH_PULL_CRON` | | Cron schedule for push/pull |
| `SYNC_SSH_KEYS_PATH` | | SSH keys path |
| `SYNC_SSH_KNOWN_HOSTS_PATH` | | SSH known hosts path |
### YAML Configuration (`config.yml`)
```yaml
exclude:
platforms: ["arcade"]
roms:
single_file:
extensions: [".txt", ".nfo"]
names: ["readme"]
multi_file:
names: ["__MACOSX"]
filesystem:
roms_folder: "roms" # Subfolder name for ROMs
firmware_folder: "bios" # Subfolder name for BIOS
skip_hash_calculation: false
system:
platforms:
snes: "snes" # fs_slug → canonical slug mappings
versions:
snes: "pal" # Platform version overrides
scan:
priority:
metadata: ["igdb", "moby", "ss"] # Provider priority
artwork: ["sgdb", "igdb", "ss"]
region: ["us", "eu", "jp"]
language: ["en", "es", "ja"]
media: ["box2d", "screenshot", "manual"]
export_gamelist: false
emulatorjs:
debug: false
netplay:
enabled: false
ice_servers:
- urls: "stun:stun.l.google.com:19302"
settings:
nes:
option_name: option_value
controls:
nes:
0: { 0: { value: "x" } }
```
Managed by `ConfigManager` (singleton pattern) which reads, validates, and writes the YAML file.
---
## 15. Error Handling
### Exception Hierarchy
```text
Exception
├── AuthCredentialsException # 401: Incorrect credentials
├── AuthenticationSchemeException # 401: Invalid auth scheme
├── UserDisabledException # 401: Account disabled
├── OAuthCredentialsException # 401: Invalid OAuth token
├── OIDCDisabledException # 500: OIDC not configured
├── OIDCNotConfiguredException # 500: OIDC feature disabled
├── PlatformNotFoundInDatabaseException # 404
├── RomNotFoundInDatabaseException # 404
├── CollectionNotFoundInDatabaseException # 404
├── CollectionPermissionError # 403
├── CollectionAlreadyExistsException # 500
├── RomNotFoundInRetroAchievementsException # 404
├── SGDBInvalidAPIKeyException # 401
├── FolderStructureNotMatchException # Invalid library layout
├── PlatformNotFoundException # Platform not found in FS
├── PlatformAlreadyExistsException # Duplicate platform
├── RomsNotFoundException # No ROMs for platform
├── RomAlreadyExistsException # Duplicate ROM
├── FirmwareNotFoundException # Firmware not found
├── ConfigNotWritableException # Config file not writable
├── SchedulerException # Task scheduling error
└── ScanStoppedException # Scan cancelled
```
### HTTP Status Codes
| Code | Meaning |
| ---- | --------------------- |
| 200 | Success (GET, PUT) |
| 201 | Created (POST) |
| 204 | No Content (DELETE) |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 409 | Conflict (duplicate) |
| 500 | Internal Server Error |
---
## 16. Logging
### Setup
**Logger name:** `"romm"`
**Level:** Configurable via `LOGLEVEL` env var (default `INFO`)
### Format
```text
[LEVEL]: [RomM][module] [timestamp] message
```
### Color Coding
| Level | Color |
| -------- | ------------- |
| DEBUG | Light Magenta |
| INFO | Green |
| WARNING | Yellow |
| ERROR | Light Red |
| CRITICAL | Red |
Color behavior:
- `FORCE_COLOR=true` → Always use colors
- `NO_COLOR=true` → Strip ANSI codes
- Default → Colors enabled
### Monitoring
- **Sentry** integration via `SENTRY_DSN` environment variable
- Release tagged as `romm@{version}`
---
## 17. Testing
### Configuration
**File:** `pytest.ini`
- Async mode enabled
- Test database: `romm_test`
- Mock API keys pre-configured
- OIDC disabled for tests
- Log level: DEBUG
### Test Structure
Tests mirror the backend directory structure under `tests/`:
```text
tests/
├── conftest.py # Shared fixtures
├── adapters/services/ # API adapter tests
│ └── cassettes/ # VCR recorded API responses
├── config/ # Configuration tests
├── endpoints/ # Endpoint integration tests
│ ├── test_auth.py
│ ├── test_collections.py
│ ├── roms/
│ └── sockets/
├── handler/ # Handler unit tests
│ ├── auth/
│ ├── database/
│ ├── filesystem/
│ └── metadata/
├── logger/ # Logger tests
├── models/ # Model tests
├── tasks/ # Task tests
└── utils/ # Utility tests
```
### Test Fixtures
Located in `romm_test/`:
- Test ROM library with real directory structure (n64, ps3, psp, psvita, psx)
- User asset fixtures
- Configuration fixtures
- VCR cassettes for external API responses (pre-recorded)
### Key Patterns
- **VCR cassettes** for external API tests (reproducible without network)
- **Test database** (separate `romm_test` DB)
- **FastAPI TestClient** for endpoint integration tests
- **Mock Redis** via FakeRedis
---
## Appendix: Key Design Patterns
| Pattern | Where | Purpose |
| --------------------------- | ------------------------------------ | --------------------------------- |
| **Three-tier architecture** | Endpoints → Handlers → Models | Separation of concerns |
| **Singleton** | `ConfigManager` | Single config instance |
| **Adapter pattern** | `adapters/services/` | Normalize external APIs |
| **Decorator pattern** | `@protected_route`, `@begin_session` | Cross-cutting concerns |
| **Context variables** | `utils/context.py` | Request-scoped state (async-safe) |
| **Repository pattern** | `handler/database/` | Encapsulate data access |
| **Observer pattern** | Socket.IO events | Real-time updates |
| **Priority queue** | RQ with 3 priority levels | Task scheduling |
| **Chunked upload** | `endpoints/roms/upload.py` | Large file handling |
| **X-Accel-Redirect** | `utils/nginx.py` | Efficient file serving |