BACKEND ARCHITECTURE
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
2. High-Level Architecture
3. Directory Structure
4. Application Lifecycle
5. Database Layer
6. API Endpoints
7. Authentication & Authorization
8. Business Logic (Handlers)
9. External Integrations (Adapters)
10. Real-Time Communication (WebSockets)
11. Background Tasks & Scheduling
12. File System Management
13. Caching (Redis)
14. Configuration
15. Error Handling
16. Logging
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
/ Detailed source-code truncated for AI context efficiency. /Layered Architecture
/ Detailed source-code truncated for AI context efficiency. /---
3. Directory Structure
/ Detailed source-code truncated for AI context efficiency. /---
4. Application Lifecycle
Startup Sequence
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 varsMiddleware Stack (execution order, outside-in)
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
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
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
/ Detailed source-code truncated for AI context efficiency. /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, SOUNDTRACK, SCREENSHOT |
| missing_from_fs | Boolean | Sync state |
Relationships: rom (M:1), track_meta (1:1, SOUNDTRACK files only)
---
#### Track Meta
Table: track_meta
Audio metadata for a SOUNDTRACK rom_file (1:1), indexed for the music API. Written from file tags at upload/scan.
| Column | Type | Notes |
| -------------------------- | ------------ | -------------------------------- |
| rom_file_id | Integer | PK, FK → rom_files (cascade) |
| rom_id | Integer | FK → roms (cascade), indexed |
| title, artist, album | String(512) | Indexed: artist, album |
| genre | String(255) | |
| year, track, disc | SmallInteger | Parsed from tags; year indexed |
| duration_seconds | Float | Indexed |
| has_embedded_cover | Boolean | |
| cover_path | String(1024) | Extracted cover under resources |
Relationships: rom_file (1:1)
---
#### 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).
It aggregates virtual_collection_roms, a real table holding one row per
(type, name, rom_id) plus that rom's cover paths. Membership is derived
from the generated_* columns on roms and maintained by triggers on that
table (virtual_collection_roms_ai/_au on MariaDB/MySQL,virtual_collection_roms_aiu calling romm_sync_virtual_collection_roms() on
PostgreSQL); rom deletions are handled by the foreign key's cascade. Reads are
therefore indexed lookups instead of a full re-derivation of every rom's
metadata. Covers are not aggregated in the view: the collections handler
resolves at most MAX_VIRTUAL_COLLECTION_COVERS per collection.
---
#### 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 | /random | ROMS_READ | Get one ROM picked at random (optionally scoped) |
| GET | /{id} | ROMS_READ | Get ROM details |
| PUT | /{id} | ROMS_WRITE | Update ROM metadata |
| POST | /{id}/convert-to-folder | ROMS_WRITE | Promote single-file ROM to a folder ROM in place |
| 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 Music (/api/music)
Music-first read API over soundtrack track_meta, for external music-player clients. All routes are visibility-filtered (hidden platforms/ROMs excluded).
| Method | Path | Scope | Description |
| ------ | ---------- | --------- | ------------------------------------------------------------------------- |
| GET | /tracks | ROMS_READ | List tracks (search + artist/album/genre/year/duration filters, sortable) |
| GET | /artists | ROMS_READ | Distinct artists + counts (searchable) |
| GET | /albums | ROMS_READ | Distinct albums + counts (searchable) |
| GET | /genres | ROMS_READ | Distinct genres + counts (searchable) |
| GET | /years | ROMS_READ | Distinct years + counts |
Facet endpoints (/artists, /albums, /genres, /years) return {value, count} and accept the same filters as /tracks, for contextual typeahead browsing.
6.7 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.8 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.9 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.10 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.11 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.12 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.13 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.14 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.15 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.16 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 |
| 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
┌──────────────────────────────────────────────────────────────┐
│ 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
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 executionCSRF 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:
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:
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: int8.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:
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
Client ←──Socket.IO──→ FastAPI (python-socketio) ←──Redis PubSub──→ WorkersScan 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_orphaned_resources | ENABLE_SCHEDULED_CLEANUP_ORPHANED_RESOURCES | 0 5 * | Remove unused artwork |
| 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 |
| sync_folder_scan | Scan sync folder for new device saves |
cleanup_orphaned_resources is also runnable this way; it is listed under
Scheduled Tasks because it additionally supports an opt-in cron schedule. It
skips the cleanup when the database reports no platforms at all while artwork
is still on disk, since that usually means the database is unavailable rather
than the library being empty. Pass {"force": true} as the request body to
clean up a genuinely emptied library.
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
{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 configurationFile 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 |
| REDIS_SAVE_POLICY | 3600 1 | Valkey snapshot policy |
#### Authentication
| Variable | Default | Description |
| ------------------------------------ | --------- | ------------------------------------- |
| ROMM_AUTH_SECRET_KEY | | Session signing key (random if unset) |
| 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_ALLOW_REGISTRATION | true | Auto-create accounts on login |
| 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)
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
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 cancelledHTTP 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
[LEVEL]: [RomM][module] [timestamp] messageColor 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/:
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 testsTest 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 |
---
FRONTEND ARCHITECTURE
RomM Frontend Architecture
Comprehensive documentation of the RomM frontend: a Vue 3 single-page application powering the retro gaming platform UI.
---
Table of Contents
1. Overview
2. High-Level Architecture
3. Directory Structure
4. Application Lifecycle
5. Routing & Navigation
6. State Management (Pinia Stores)
7. API & Data Layer
8. Component Architecture
9. Views & Pages
10. Console Mode
11. Emulation Integration
12. Theming & Styling
13. Internationalization (i18n)
14. Real-Time Communication
15. Caching Strategy
16. Utilities & Composables
17. Build & Tooling
18. Type System
---
1. Overview
| Property | Value |
| -------------------- | ---------------------------------------------- |
| Framework | Vue 3.4.27 (Composition API, <script setup>) |
| Build Tool | Vite 6.4.2 |
| Language | TypeScript 5.7.3 (noImplicitAny: true) |
| UI Library | Vuetify 3.9.2 (Material Design) |
| CSS | Tailwind CSS 4.0.0 + Vuetify themes |
| State Management | Pinia 3.0.1 (18 stores) |
| Routing | Vue Router 4.3.2 |
| HTTP Client | Axios 1.15.0 |
| i18n | vue-i18n 11.1.10 (17 languages) |
| Real-time | Socket.IO Client 4.7.5 |
| Icons | Material Design Icons (MDI) 7.4.47 |
| Node | 24 (via .nvmrc) |
Total: ~216 Vue components (168 under components/, rest in views/console/layouts), 18 Pinia stores, 17 API service modules, 36 named routes across 3 layouts.
---
2. High-Level Architecture
/ Detailed source-code truncated for AI context efficiency. /Layered Architecture
┌─────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ views/ Page-level route components │
│ layouts/ Auth, Main, Console layouts │
│ components/ Feature & common components │
│ console/ TV/gamepad-optimized UI │
├─────────────────────────────────────────────────────────┤
│ STATE LAYER │
│ stores/ 18 Pinia stores (auth, roms, config...) │
│ composables/ Reusable stateful logic │
├─────────────────────────────────────────────────────────┤
│ DATA LAYER │
│ services/api/ 17 Axios-based API modules │
│ services/cache/ Browser Cache API wrapper │
│ services/socket Socket.IO client │
├─────────────────────────────────────────────────────────┤
│ INFRASTRUCTURE LAYER │
│ plugins/ Vuetify, Pinia, i18n, Router │
│ styles/ Themes, global CSS │
│ locales/ 17 language packs │
│ types/ TypeScript definitions │
│ utils/ Helpers (formatting, emulation, covers) │
│ __generated__/ OpenAPI-generated types │
└─────────────────────────────────────────────────────────┘---
3. Directory Structure
/ Detailed source-code truncated for AI context efficiency. /---
4. Application Lifecycle
Startup Sequence
index.html
└── <script type="module" src="src/main.ts">
│
├── Create Vue app with RomM.vue as root
├── Register plugins (Vuetify, Pinia, i18n, Mitt, MD Editor)
├── Install Vue Router
│
├── Initialize critical stores (before mount):
│ ├── authStore.fetchCurrentUser()
│ ├── configStore.fetchConfig()
│ ├── heartbeatStore.fetchHeartbeat()
│ └── tasksStore.fetchTasks()
│
└── app.mount("#app")Plugin Registration Order
1. Vuetify : Material Design components, themes, icons
2. Pinia : State management (with router injection)
3. vue-i18n : Internationalization (17 locales)
4. Mitt : Event emitter (provided as 'emitter')
5. MD Editor : Markdown editor with XSS plugin
6. Vue Router : Navigation with guardsRequest Lifecycle
Component Action
│
├─ Store Action (e.g., romsStore.fetchRoms())
│ │
│ ├─ Cache check (if experimental cache enabled)
│ │ ├─ Cache hit → return cached, fire background update
│ │ └─ Cache miss → continue to API
│ │
│ ├─ API Service call (e.g., romApi.getRoms(params))
│ │ │
│ │ ├─ Axios request interceptor:
│ │ │ ├─ Add CSRF token (x-csrftoken from cookie)
│ │ │ └─ Track in inflight set
│ │ │
│ │ ├─ HTTP request to /api/*
│ │ │
│ │ └─ Axios response interceptor:
│ │ ├─ Remove from inflight set
│ │ ├─ 403 → clear session, redirect to login
│ │ └─ Emit 'network-quiesced' when all requests complete
│ │
│ └─ Store mutation (update reactive state)
│
└─ Component reacts via reactive refs/getters---
5. Routing & Navigation
Route Map
/ Detailed source-code truncated for AI context efficiency. /Route Guards
| Guard | Type | Behavior |
| ---------------------- | ------------- | --------------------------------------------------- |
| Global beforeEach | Navigation | Setup wizard redirect, auth check, scope validation |
| ROM beforeEnter | Per-route | Pre-fetches ROM data before rendering |
| Global beforeResolve | Navigation | View Transitions API animation |
| Scroll behavior | Router config | Restores saved scroll position on back/forward |
Permission-Protected Routes
| Route | Required Scope |
| --------------------- | ----------------- |
| /scan | platforms.write |
| /library-management | platforms.write |
| /client-api-tokens | me.write |
| /administration | users.write |
---
6. State Management (Pinia Stores)
Store Overview
┌─────────────────────────────────────────────────────┐
│ PINIA STORES │
├──────────────┬──────────────────────────────────────┤
│ Core Data │ roms, platforms, collections, users │
├──────────────┼──────────────────────────────────────┤
│ Auth & Config│ auth, config, heartbeat │
├──────────────┼──────────────────────────────────────┤
│ UI State │ navigation, galleryFilter, galleryView│
│ │ language, notifications, console │
├──────────────┼──────────────────────────────────────┤
│ Operations │ scanning, tasks, upload, download, │
│ │ playing │
└──────────────┴──────────────────────────────────────┘Key Stores in Detail
#### roms (largest store, ~400 lines)
| State | Type | Description |
| -------------------------------- | ------------------------ | -------------------------------- |
| _allRoms | SimpleRom[] | Current page of ROMs |
| currentPlatform | Platform \| null | Active platform filter |
| currentCollection | Collection \| null | Active collection filter |
| currentRom | DetailedRom \| null | Selected ROM details |
| recentRoms | SimpleRom[] | Recently added |
| continuePlayingRoms | SimpleRom[] | Recently played |
| selectedIDs | Set<number> | Multi-select state |
| fetchOffset / fetchTotalRoms | number | Pagination cursor |
| orderBy / orderDir | string | Sort (persisted to localStorage) |
| characterIndex | Record<string, number> | A-Z jump index |
Key actions: fetchRoms(), fetchRecentRoms(), fetchContinuePlayingRoms(), add(), update(), remove(), resetPagination()
#### galleryFilter
Manages 13+ filter dimensions with logic operators:
| Filter | Type | Logic |
| ----------------- | ----------------- | ---------------- |
| Genres | string[] | any / all / none |
| Franchises | string[] | any / all / none |
| Collections | string[] | any / all / none |
| Companies | string[] | any / all / none |
| Age Ratings | string[] | any / all / none |
| Regions | string[] | any / all / none |
| Languages | string[] | any / all / none |
| Player Counts | string[] | any / all / none |
| Statuses | string[] | any / all / none |
| Matched | boolean \| null | toggle |
| Favorites | boolean \| null | toggle |
| Duplicates | boolean \| null | toggle |
| Playable | boolean \| null | toggle |
| RetroAchievements | boolean \| null | toggle |
| Missing | boolean \| null | toggle |
| Verified | boolean \| null | toggle |
#### collections
Manages three collection types:
| Type | State | Description |
| -------- | -------------------- | -------------------------------- |
| Regular | allCollections | User-created collections |
| Virtual | virtualCollections | Auto-generated by platform/genre |
| Smart | smartCollections | Filter-criteria based |
| Favorite | favoriteCollection | Special favorite collection |
#### heartbeat
Server capability flags used throughout the UI:
METADATA_SOURCES: { IGDB, SS, MOBY, RA, STEAMGRIDDB, LAUNCHBOX, ... }
EMULATION: { DISABLE_EMULATOR_JS, DISABLE_RUFFLE_RS }
FRONTEND: { DISABLE_USERPASS_LOGIN, DISABLE_LOGS_VIEWER, YOUTUBE_BASE_URL }
OIDC: { ENABLED, AUTOLOGIN, PROVIDER, RP_INITIATED_LOGOUT }
TASKS: { scheduled task configurations }Persistence Strategy
| Storage | What | Examples |
| ------------------------------ | ------------------ | ----------------------------------------------------------- |
| localStorage | UI preferences | View mode, sort order, theme, drawer state, boxart style |
| Backend (user.ui_settings) | Synced preferences | Same as localStorage, synced via useUISettings composable |
| In-memory (Pinia) | Session data | ROMs, platforms, collections, auth state |
| Browser Cache API | API responses | Optional experimental cache with background updates |
---
7. API & Data Layer
Axios Client Setup
Location: services/api/index.ts
const api = axios.create({
baseURL: "/api",
timeout: 120000, // 2 minutes
});Request Interceptor:
- Injects CSRF token from romm_csrftoken cookie as x-csrftoken header
- Tracks inflight requests in a Set
Response Interceptor:
- On 403: clears session cookie, refetches CSRF, redirects to /login
- Fires network-quiesced custom event when all requests complete (250ms debounce)
API Service Modules
| Module | Key Endpoints |
| ----------------- | ------------------------------------------------ |
| rom.ts | CRUD, chunked upload, download, search, notes |
| collection.ts | CRUD for regular/smart/virtual + ROM association |
| platform.ts | CRUD, supported list |
| user.ts | CRUD, profile, RA refresh, invite links |
| identity.ts | Login, logout, forgot/reset password |
| config.ts | Platform bindings, versions, exclusions |
| task.ts | List, status, run |
| firmware.ts | Upload, list, delete |
| save.ts | Upload, update, delete |
| state.ts | Upload, update, delete |
| screenshot.ts | Upload, update |
| setup.ts | Library structure, platform creation |
| sgdb.ts | Cover art search |
| export.ts | Gamelist.xml export, Pegasus export |
| play-session.ts | Play session ingestion & listing |
| client-token.ts | Token CRUD, pair, exchange |
Chunked Upload System (rom.ts)
1. POST /roms/upload/start
Headers: X-Upload-Filename, X-Upload-Total-Size, X-Upload-Total-Chunks
→ Returns upload_id2. PUT /roms/upload/{upload_id} (per 10MB chunk)
Headers: X-Chunk-Number, X-Chunk-Size
→ Retry: 3 attempts with exponential backoff
3. POST /roms/upload/{upload_id}/complete
→ 10-minute timeout for assembly
On failure: POST /roms/upload/{upload_id}/cancel
Key Data Flows
ROM Gallery Loading:
Component mount → romsStore.fetchRoms()
→ cachedApiService.getRoms(params, onBackgroundUpdate)
→ Cache hit? Return cached + background refresh
→ API call: GET /api/roms?platform_id=...&limit=72&offset=0&...
→ _postFetchRoms(): update ROMs, pagination, character index, filter values
→ Components react via reactive gettersFilter & Search:
User sets filter → galleryFilterStore.setSelected*()
→ Component detects change → romsStore.fetchRoms()
→ _buildRequestParams() merges all 13+ filter dimensions
→ API returns filtered paginated results
→ _postFetchRoms() updates available filter values from responseSettings Sync:
User changes setting → localStorage updated
→ useUISettings watcher fires
→ PUT /api/users/{id} with ui_settings JSON
→ Backend returns updated user
→ authStore.setCurrentUser(data)
→ On next login: user.ui_settings hydrates localStorage---
8. Component Architecture
Organization Pattern
Feature-based hybrid with three tiers:
Tier 1: Common (shared, reusable)
├── Collection/ Cards, list items, 6 dialogs
├── Dialog/ Loading, SearchCover
├── EmptyStates/ 8 variants (game, platform, collection, firmware, saves...)
├── Game/ Cards, 14 dialogs, PlayBtn, FavBtn, VirtualTable (48+)
├── Navigation/ AppBar, 3 drawers, 10 nav buttons
├── Platform/ Cards, PlatformIcon, 3 dialogs
└── Notifications/ Snackbar, upload progressTier 2: Feature-specific
├── Details/ Game detail tabs (14+ sub-components)
├── Gallery/ AppBar variants, filters, skeleton
├── Home/ Dashboard sections (8 components)
├── Scan/ Scan platform component
└── Settings/ 25+ settings sub-components
Tier 3: Console Mode
└── console/ 12 components + 7 composables + input system
Component Communication
┌─────────────────┐ props/emit ┌─────────────────┐
│ Parent │ ←───────────────→ │ Child │
│ Component │ │ Component │
└────────┬────────┘ └────────┬────────┘
│ │
store refs store refs
│ │
v v
┌─────────────────────────────────────────────────────────┐
│ Pinia Stores │
└─────────────────────────────────────────────────────────┘
│
mitt events (80+ types)
│
v
┌─────────────────────────────────────────────────────────┐
│ Cross-Component Events │
│ showEditRomDialog, snackbarShow, playGame, etc. │
└─────────────────────────────────────────────────────────┘Patterns used:
- Props/emit for parent-child communication
- Pinia stores for shared state across components
- Mitt emitter for loosely-coupled cross-component events (dialog triggers, notifications)
- Provide/inject for console input scoping
Dialog System
All dialogs use Vuetify's v-dialog wrapped in a custom RDialog component:
RDialog (wrapper)
├── Header slot (title + close button)
├── Toolbar slot (optional)
├── Prepend slot
├── Content slot (scrollable)
├── Append slot
└── Footer slot (actions)15 game dialogs: EditRom (with 4 sub-components), UploadRom, DeleteRom, MatchRom, NoteDialog, ShowQRCode, CopyDownloadLink, SelectSave, UploadSaves, DeleteSaves, SelectState, UploadStates, DeleteStates
All triggered via Mitt events, rendered in Main.vue layout.
---
9. Views & Pages
Home Dashboard (/)
| Section | Data Source | Toggleable |
| ------------------- | -------------------------------------- | ------------------ |
| Stats cards | GET /api/stats | Yes (localStorage) |
| Recently added | romsStore.fetchRecentRoms() | Yes |
| Continue playing | romsStore.fetchContinuePlayingRoms() | Yes |
| Platforms grid | platformsStore | Yes |
| Collections | collectionsStore | Yes |
| Smart collections | collectionsStore | Yes |
| Virtual collections | collectionsStore | Yes |
Platform Gallery (/platform/:platform)
- Grid or table view (3 sizes + list)
- Infinite scroll pagination (72 per page)
- Multi-select for bulk operations
- 3D tilt effect on cards (vanilla-tilt)
- Virtual table for list mode performance
Game Details (/rom/:rom)
8-tab interface:
| Tab | Content |
| ------------------ | --------------------------------- |
| Details | File info + game metadata |
| Manual | PDF viewer (if available) |
| Save Data | Save file management |
| Personal | Notes, rating, play time, status |
| How Long To Beat | Playtime estimates (if HLTB data) |
| Additional Content | DLC/expansions (mobile) |
| Related Games | Remakes/remasters (mobile) |
| Screenshots | Screenshot gallery |
Scan (/scan)
- Platform multi-select
- Metadata source selection with priority ordering
- Real-time progress via Socket.IO
- Log auto-scroll
- Hash calculation toggle
ROM Patcher (/patcher)
Supports: .ips, .ups, .bps, .ppf, .rup, .aps, .bdf, .pmsr, .vcdiff
- Drag-and-drop ROM + patch files
- Platform selection for output
- Save locally or upload to RomM
---
10. Console Mode
A complete TV/gamepad-optimized interface under /console/.
Architecture
Console Layout
├── Input Bus (keyboard + gamepad → actions)
├── Theme System (CSS variables per theme)
├── Spatial Navigation (grid-based focus)
├── Sound Effects (Web Audio synthesis)
│
├── Home View
│ ├── Platform cards (spatial nav)
│ ├── Continue playing
│ └── Collections grid
│
├── Games List View
│ ├── Game cards with lazy loading
│ └── Virtual scrolling
│
├── Game Detail View
│ ├── Description, metadata, screenshots
│ ├── Save state management
│ └── Play button → Emulator
│
└── Play View
└── EmulatorJS with save/state/BIOS selectionInput System
Hardware Input (keyboard / gamepad)
│
├── Keyboard Listener (keydown → action mapping)
│ └── Ignores when focused on INPUT/TEXTAREA
│
├── Gamepad Poller (requestAnimationFrame loop)
│ ├── Button press detection (with repeat delay)
│ └── Analog stick threshold (0.2)
│
└── Input Bus (stack-based scope manager)
├── Global shortcuts (always active)
├── Scoped listeners (context-dependent)
└── Action dispatch with SFX feedback12 Input Actions: moveUp, moveDown, moveLeft, moveRight, confirm, back, menu, delete, tabNext, tabPrev, toggleFavorite
Repeat Timing: 350ms initial delay, 120ms repeat
Procedural Sound Effects (Web Audio API)
| Sound | Frequency | Duration | When |
| ---------- | --------------- | ---------- | ------------------ |
| move | 860Hz | 20ms | Navigation |
| confirm | 680→880Hz sweep | 19ms | Selection |
| back | 300Hz | 85ms | Return |
| error | 180Hz + 140Hz | 180ms | Failure |
| delete | 260Hz + 180Hz | 120ms | Destructive action |
| favorite | 600Hz + 950Hz | Dual burst | Toggle |
All synthesized with sine/noise blend, exponential envelopes, low-pass filter, and waveshaper saturation.
Console Composables
| Composable | Purpose |
| -------------------- | ----------------------------------------------- |
| useSpatialNav | Grid navigation with boundary enforcement |
| useConsoleTheme | Theme CSS variable injection |
| useThemeAssets | Format-aware asset resolution (SVG > PNG > JPG) |
| useBackgroundArt | Double-buffered background transitions |
| useElementRegistry | Focus element tracking per section |
| useInputScope | Dependency-injected input subscription |
| useRovingDom | ARIA roving tabindex with auto-scroll |
---
11. Emulation Integration
EmulatorJS
Location: views/Player/EmulatorJS/
| Feature | Details |
| --------------- | ---------------------------------------------- |
| Core selection | Platform-specific core mapping (40+ platforms) |
| BIOS/firmware | Selectable from uploaded firmware |
| Save management | Upload, download, delete saves & states |
| Multi-disc | Disc selection for multi-file games |
| Cache | IndexedDB cache for game data |
| Fullscreen | With keyboard lock |
| Netplay | Socket.IO-based multiplayer |
| Controls | Per-core configurable via config.yml |
Ruffle (Flash)
Location: views/Player/RuffleRS/
- SWF/Flash game emulation via Ruffle 0.2.0-nightly
- Fullscreen support
- Background color customization
Platform Detection
utils/index.ts provides:
- getSupportedEJSCores(platform): maps platforms to EmulatorJS cores
- isEJSEmulationSupported(rom): checks WebGL + server config
- isRuffleEmulationSupported(rom): checks Flash platform
- isCDBasedSystem(platform): 31 CD-based platforms for animation logic
---
12. Theming & Styling
Theme System
Location: styles/themes.ts
| Theme | Background | Primary | Accent |
| ----- | ---------- | --------- | --------- |
| Dark | #0D1117 | #8B74E8 | #E1A38D |
| Light | #F2F4F8 | #371f69 | #E1A38D |
Detection priority: settings.theme localStorage → prefers-color-scheme media query → dark default
Vuetify handles theme switching. Additional shared brand colors: romm-red, romm-green, romm-blue, romm-gold.
CSS Stack
| Layer | Technology | Scope |
| --------- | ---------------------------------- | ---------------------- |
| Component | Vuetify classes + scoped <style> | Per-component |
| Utility | Tailwind CSS 4.0 | Inline utility classes |
| Global | styles/common.css | App-wide utilities |
| Scrollbar | styles/scrollbar.css | Custom scrollbar |
| Console | console/index.css | Console mode only |
Procedural Cover Generation
utils/covers.ts generates SVG covers with:
- Hash-based deterministic gradients (consistent per game)
- Collection covers with multi-image grid
- Favorite covers with star icon
- Missing/unmatched covers with icons
- Aspect-ratio-aware empty placeholders
---
13. Internationalization (i18n)
Setup
- Library: vue-i18n 11.1.10 (Composition API mode)
- Locale loading: Dynamic glob import from locales/{lang}/*.json
- Default: en_US
- Fallback: en_US
Supported Languages (17)
| Code | Language |
| ------- | --------------------------- |
| en_US | English (US, default) |
| en_GB | English (UK) |
| fr_FR | French |
| de_DE | German |
| es_ES | Spanish |
| it_IT | Italian |
| ja_JP | Japanese |
| ko_KR | Korean |
| pt_BR | Portuguese (Brazil) |
| pl_PL | Polish |
| ro_RO | Romanian |
| ru_RU | Russian |
| zh_CN | Chinese (Simplified) |
| zh_TW | Chinese (Traditional) |
| cs_CZ | Czech (custom plural rules) |
| hu_HU | Hungarian |
| bg_BG | Bulgarian |
Namespace Organization
Each locale directory contains translation files per feature:collection, common, console, detail, emulator, gallery, home, library, login, navigation, patcher, platform, scan, settings, task
---
14. Real-Time Communication
Socket.IO Client
Location: services/socket.ts
io({
path: "/ws/socket.io/",
transports: ["websocket", "polling"],
autoConnect: false,
});Usage: Manually connected during upload and scan operations.
Events consumed:
- scan:update_stats: live scan progress (platform/ROM counts)
- scan:log: scan log messages
- scan:stop: scan completion
Dev proxy: Vite proxies /ws to backend with WebSocket upgrade support.
---
15. Caching Strategy
Experimental Browser Cache
Location: services/cache/
Opt-in: localStorage.settings.enableExperimentalCache
Request Flow with Cache:
┌──────────┐ cache hit ┌──────────┐
│ Component├───────────────→│ Cached │ → Immediate render
│ │ │ Response │
│ │ meanwhile │ │
│ │◄───────────────│ Background│ → API fetch
│ │ onBackgroundUpdate │ → Update if different
└──────────┘ └──────────┘Features:
- Browser Cache API (requires HTTPS)
- Request deduplication (concurrent identical requests share promise)
- Background update callbacks (stale-while-revalidate pattern)
- Pattern-based cache clearing
- Used for ROM lists and recent/continue playing data
---
16. Utilities & Composables
Global Composables
| Composable | Purpose | Key Features |
| ------------------- | -------------------- | ------------------------------------------------------------------ |
| useUISettings | Settings persistence | Singleton, localStorage ↔ backend bidirectional sync, 25+ settings |
| useFavoriteToggle | Favorites management | Auto-creates Favorites collection, toggle with notifications |
| useGameAnimation | Card animations | CD spin (5000 deg/s), cartridge load, video hover (1.5s delay) |
| useAutoScroll | Scroll management | Throttled (50ms), mutation observer, respects user scroll |
Utility Functions (utils/index.ts, ~825 lines)
Display:
- formatBytes(): human-readable sizes (B through PB)
- formatTimestamp(): locale-aware dates
- formatRelativeDate(): relative time strings
Emojis & Localization:
- regionToEmoji(): 50+ region codes → country flags
- languageToEmoji(): 40+ language codes → country flags
Emulation Support:
- getSupportedEJSCores(): platform → EmulatorJS core mapping
- isEJSEmulationSupported(): WebGL + config check
- isCDBasedSystem(): 31 CD-based platforms
- isArcadeSystem(): 3 arcade platforms
Game Status:
- romStatusMap: 8 statuses with emoji, text, i18n keys
- Status enum: unplayed, now_playing, backlogged, paused, completed, 100%, retired, never_playing
Layout:
- views: 3 view modes with responsive grid configurations
- calculateMainLayoutWidth(): dynamic width based on drawer state
Task Display:
- convertCronExpression(): human-readable cron (via cronstrue)
- Task status/type maps with colors and icons
Cover Generation (utils/covers.ts)
Procedural SVG generation for:
- Collection covers (multi-image grid with deterministic gradients)
- Favorite covers (star icon themed)
- Missing covers (question mark icon)
- Unmatched covers (warning icon)
- Empty placeholders (aspect-ratio-aware)
---
17. Build & Tooling
Vite Configuration
| Feature | Config |
| ------------------- | ---------------------------------------- |
| Target | ESNext |
| Dev port | 3000 (8443 with HTTPS) |
| Backend proxy | /api/* → http://127.0.0.1:5000 |
| WebSocket proxy | /ws, /netplay → backend with upgrade |
| Allowed hosts | localhost, 127.0.0.1, romm.dev |
Plugins:
1. Tailwind CSS (@tailwindcss/vite)
2. Vue 3 (@vitejs/plugin-vue)
3. Vuetify auto-import (vite-plugin-vuetify, 57 pre-optimized components)
4. PWA (vite-plugin-pwa, service worker, installable)
5. HTTPS (vite-plugin-mkcert, optional dev HTTPS)
6. Static copy (ROM patcher JS assets)
Scripts
| Script | Command | Purpose |
| ----------- | ---------------------------- | ----------------------------------- |
| dev | vite --host | Development server |
| build | vite build | Production build |
| preview | vite preview | Preview production build |
| typecheck | vue-tsc | TypeScript validation |
| generate | openapi-typescript-codegen | Generate types from backend OpenAPI |
| lint | eslint | Lint .vue, .js, .ts files |
OpenAPI Code Generation
npm run generate
Fetches http://127.0.0.1:3000/openapi.json
Generates TypeScript interfaces in __generated__/models/
Generated types used throughout stores and API services for type-safe backend communication.
ESLint Configuration
- Flat config (eslint.config.js)
- Vue plugin with essential rules
- TypeScript-ESLint integration
- Vue accessibility plugin (eslint-plugin-vuejs-accessibility)
---
18. Type System
Generated Types (__generated__/models/)
Auto-generated from backend OpenAPI schema:
| Type | Description |
| ------------------------------------------------- | -------------------------------------------------- |
| SimpleRomSchema | ROM in list view (covers, metadata IDs, user data) |
| DetailedRomSchema | Full ROM with all relationships |
| SearchRomSchema | Minimal search result |
| PlatformSchema | Platform with ROM count |
| UserSchema | User with role and settings |
| CollectionSchema | Collection with ROM IDs |
| VirtualCollectionSchema | Auto-generated collection |
| SmartCollectionSchema | Filter-based collection |
| SaveSchema / StateSchema / ScreenshotSchema | Asset types |
| FirmwareSchema | BIOS file info |
| HeartbeatResponse | Server status and capabilities |
| ConfigResponse | Full server configuration |
| ScanStats | Scan progress counters |
| TaskInfo / TaskStatusResponse | Background task data |
| GetRomsResponse | Paginated ROM list with filter values |
Custom Types
| File | Types |
| ----------------- | ------------------------------------------------------ |
| emitter.d.ts | SnackbarStatus, Events (80+ event signatures) |
| rom.d.ts | RomSelectEvent |
| user.d.ts | UserItem (extends User with password + avatar) |
| ruffle.d.ts | RufflePlayerElement, RuffleSourceAPI |
| rompatcher.d.ts | ROM patching library interfaces |
| main.d.ts | Global augmentations |
| index.ts | isKeyof<T>, ExtractPiniaStoreType<D>, ValueOf<T> |
Path Alias
"@/" → "./src/"Used throughout: import { ... } from "@/stores/roms".
---
Appendix: Key Design Patterns
| Pattern | Where | Purpose |
| -------------------------- | ---------------------- | ------------------------------------------------ |
| Composition API | All components | <script setup> with reactive refs |
| Pinia stores | stores/ | Centralized state with actions/getters |
| Mitt event bus | Cross-component | Loosely-coupled dialog/notification triggers |
| Composables | composables/ | Reusable stateful logic (singleton where needed) |
| Stale-while-revalidate | services/cache/ | Return cached, update in background |
| Chunked upload | services/api/rom.ts | 10MB chunks with retry |
| Spatial navigation | console/ | Grid-based focus for gamepad/keyboard |
| Input scoping | console/input/bus.ts | Stack-based context for input handling |
| Procedural audio | console/utils/sfx.ts | Web Audio API synthesis |
| Double buffering | useBackgroundArt | Smooth background transitions |
| View Transitions | plugins/transition/ | CSS View Transitions API |
| OpenAPI codegen | __generated__/ | Type-safe API communication |
| Feature flags | heartbeatStore | Server-driven UI feature toggling |
---
CONTRIBUTING
Contributing to RomM
Thank you for considering contributing to RomM! This document outlines some guidelines to help you get started with your contributions.
If you're looking to implement a large feature or make significant changes to the project, it's best to open an issue first AND join the Discord to discuss your ideas with the maintainers.
Code of Conduct
Please note that this project adheres to the Contributor Covenant code of conduct. By participating in this project, you are expected to uphold this code.
AI Assistance Notice
> If you are using any kind of AI assistance to contribute to RomM, it must be disclosed in the pull request.
If you are using any kind of AI assistance while contributing to RomM this must be disclosed in the pull request, along with the extent to which AI assistance was used (e.g. docs only vs. code generation). If PR responses are being generated by an AI, disclose that as well. As a small exception, trivial tab-completion doesn't need to be disclosed.
An example disclosure:
This PR was written primarily by Claude Code.
Or a more detailed disclosure:
I consulted ChatGPT to understand the codebase but the solution
was fully authored manually by myself.
Failure to disclose this is rude to the human operators on the other end of the pull request, but it also makes it difficult to determine how much scrutiny to apply to the contribution.
In a perfect world, AI assistance would produce equal or higher quality work than any human. That isn't the world we live in today, and in most cases it's generating slop.
Please be respectful to maintainers and disclose AI assistance.
Contributing to the Docs
If you would like to contribute to the project's documentation, open a pull request against the docs repo. We welcome any contributions that help improve the documentation (new pages, updates, or corrections).
Adding Translations
If you would like to translate the project into another language, create a new folder under the frontend/src/locales directory, and follow the existing language files as a template. Once you've created the new language file, open a pull request to add it to the project.
How to Contribute Code
1. Fork the repository.
2. Clone your forked repository: git clone https://github.com/your-username/romm.git
3. Checkout the master branch: git checkout master
4. Follow the steps in the developer setup guide
5. Create a new branch for your feature/fix: git checkout -b feature-or-fix-name
6. Make your changes and commit them with descriptive commit messages: git commit -am 'Add feature XYZ'
7. Push your changes to your fork: git push origin feature-or-fix-name
8. Open a pull request to the master branch of the original repository.
Pull Request Guidelines
- Make sure your code follows the project's coding standards.
- Test your changes locally before opening a pull request.
- Update the documentation if necessary.
- Ensure all existing tests pass, and add new tests for new functionality.
- Use clear and descriptive titles and descriptions for your pull requests.
Code Style
Follow the existing code style used throughout the project. If working with VSCode or a similar editor, consider installing these extensions:
- Prettier
- Python
- Pylance
- Ruff
- Vue - Official
Issue Reporting
If you encounter any bugs or have suggestions for improvements, please create an issue on GitHub. Provide as much detail as possible, including steps to reproduce the issue if applicable.
Licensing
By contributing to RomM, you agree that your contributions will be licensed under the project's LICENSE.
---
Thank you for contributing to RomM! Your help is greatly appreciated.
---
SECURITY
Reporting Security Issues
Thanks for helping make RomM safer for everyone.
If you believe you have found a security vulnerability in RomM, please report it to us through coordinated disclosure.
Do not report security vulnerabilities through public GitHub issues, discussions, pull requests, or on our public Discord server.
Instead, use the vulnerability report form on GitHub.
Please include as much of the information listed below as you can to help us better understand and resolve the issue:
- The type of issue (e.g., permission bypass, remote code execution, etc.)
- Full paths of source file(s) related to the manifestation of the issue
- The location of the affected source code (tag/branch/commit or direct URL)
- Any special configuration required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue (including how an attacker might exploit the issue)
This information will help us investigate and patch the issue more quickly.
---