Download Latest Version 5.4.1 source code.zip (36.7 MB)
Home / 5.4.0
Name Modified Size InfoDownloads / Week
Parent folder
5.4.0 source code.tar.gz 2026-10-10 34.7 MB
5.4.0 source code.zip 2026-10-10 36.7 MB
README.md 2026-10-10 68.7 kB
Totals: 3 Items   71.5 MB 4

[!CAUTION] Give the first start 15-20 mintes to finish its migrations. Several of this release's migrations rewrite the roms table and backfill it in batches, which can take a while on a large library. Don't restart the container while they run.

[!WARNING] Cross-origin requests are denied by default. An unset or empty ROMM_CORS_ALLOWED_ORIGINS used to allow every origin, and now allows none. If a browser-based client on another origin talks to your RomM, list its origin in ROMM_CORS_ALLOWED_ORIGINS. A * still answers any origin, but without credentials, so a client that signs in with the session cookie needs its origin listed explicitly. [#4699]

[!WARNING] Add a reverse proxy on a public address to FORWARDED_ALLOW_IPS. RomM now trusts X-Forwarded-For only from loopback and private ranges. A proxy outside those ranges has to be added, or every request is logged and rate limited as coming from the proxy's address. [#4748]

Technical changes

🌎 ENVIRONMENT VARIABLES #### Notifications and email | variable | default | description | | --- | --- | --- | | SMTP_HOST | `-` | SMTP server; email stays off until this and `SMTP_FROM` are set | | SMTP_PORT | `587` | SMTP server port | | SMTP_USERNAME | `-` | Login for the SMTP server, if it needs one | | SMTP_PASSWORD | `-` | Password for the SMTP server | | SMTP_FROM | `-` | Sender address, such as `romm@example.com` | | SMTP_SECURITY | `starttls` | `starttls`, `tls` (implicit TLS, usually port 465) or `none` | #### Audit log | variable | default | description | | --- | --- | --- | | AUDIT_LOG_RETENTION_DAYS | `90` | Days to keep audit events; `0` keeps them forever | #### Library conversion | variable | default | description | | --- | --- | --- | | ROM_CONVERTO_ENABLED | `false` | Enable the "Convert library" task and `?format=` download conversions | | ROM_CONVERTO_TIMEOUT | `600` | Seconds per rom-converto operation | | ROM_CONVERTO_MAX_CONCURRENCY | `2` | Concurrent conversions per web or task worker | #### Devices and sync | variable | default | description | | --- | --- | --- | | DEVICE_INSTALL_ENABLED | `true` | Let users push a ROM to one of their devices for install | | DEVICE_INSTALL_REQUEST_TTL_DAYS | `2` | Days an unfinished install request waits for its device; `0` waits forever | | DEVICE_INSTALL_EXCLUDED_PLATFORM_SLUGS | `win,win3x,win9x,windows-apps` | Platforms that can never be pushed to a device | | SYNC_RETROARCH_PSP_SERIAL_MAP | `{}` | JSON map of PSP serial to extensionless ROM file name, for RetroArch PSP saves whose title matches no ROM | #### Scheduled cleanups | variable | default | description | | --- | --- | --- | | ENABLE_SCHEDULED_CLEANUP_NETPLAY | `true` | Clean up empty netplay rooms | | SCHEDULED_CLEANUP_NETPLAY_CRON | `*/30 * * * *` | When that cleanup runs | | ENABLE_SCHEDULED_CLEANUP_UPLOAD_TMP | `true` | Clean up abandoned chunked uploads | | SCHEDULED_CLEANUP_UPLOAD_TMP_CRON | `0 * * * *` | When that cleanup runs | | ENABLE_SCHEDULED_CLEANUP_ZIP_CACHE | `true` | Clean up stale cached ZIP files | | SCHEDULED_CLEANUP_ZIP_CACHE_CRON | `0 4 * * *` | When that cleanup runs | | ENABLE_SCHEDULED_CLEANUP_SYNC_SESSIONS | `true` | Fail sync sessions no client completed | | SCHEDULED_CLEANUP_SYNC_SESSIONS_CRON | `23 * * * *` | When that cleanup runs | #### General | variable | default | description | | --- | --- | --- | | ⚠️ ROMM_CORS_ALLOWED_ORIGINS | `-` | Empty now allows no cross-origin requests; a `*` allows any origin but never credentials | | ⚠️ FORWARDED_ALLOW_IPS | local/loopback addresses | Proxies trusted to report the client's address; was `*` | | DISABLE_EASYRPG | `false` | Disable the EasyRPG player for everyone | | SEVEN_ZIP_TIMEOUT | `180` | Timeout for 7-Zip operations in seconds, was `60` |
📡 API CHANGES | Change | Description | | --- | --- | | ⚠️ CORS denied by default | `ROMM_CORS_ALLOWED_ORIGINS` now defaults to empty instead of `*`; browser clients on another origin must be allowlisted (#4699) | | CORS wildcard drops credentials | With `*` in `ROMM_CORS_ALLOWED_ORIGINS`, responses no longer send `Access-Control-Allow-Credentials` (#4699) | | Invalid credentials are unauthenticated | A malformed Basic header or an invalid/expired JWT bearer no longer 500s; the request proceeds unauthenticated and gets the route's 401/403 (#4799) | | ⚠️ `PermissionGroupSchema.is_system` removed | Replaced by `system_key` (`SystemGroupKey` or null) (#4767) | | Age limits on permissions | `PermissionGroupSchema/Create/Update` and `UserPermissionsSchema/Update` gain `age_limit`, `hide_unrated_roms`; updates apply them only with `set_age_settings: true` (#5081) | | Collection `rom_count` is computed | `rom_count` on collection, smart and virtual collection schemas is now read-only, derived from the visible `rom_ids` (#4930) | | ⚠️ Collection name errors | Duplicate name on create or rename returns 409 (was 500); names over 400 chars return 422 (#4997) | | ⚠️ `GET /api/netplay/list` | Scope `assets.read` - `roms.read`; `game_id` is now an integer ROM id, 404 for a missing or hidden ROM (#4700) | | ⚠️ Netplay socket rooms | `open-room` and password-less `join-room` require a signed-in user with `roms.read` who can see the ROM; the room password moved from `extra.room_password` to top-level `password` (#4700) | | ⚠️ `GET /api/play-sessions` | Without `devices.read`, only allowed when `device_id` equals the calling token's device; no longer defaults `device_id` from the token (#4956) | | ⚠️ `PlaySessionSchema.sync_session_id` removed | Field dropped from play session responses (#4670) | | Page params on play and sync sessions | `GET /api/play-sessions` and `GET /api/sync/sessions` use shared page params (`limit` 1 to 10000, `offset`); sync sessions gains `offset` (#4715) | | ⚠️ Sync `delete` operation | `SyncOperationSchema.action` adds `delete` (slot emptied on the server); `SyncNegotiateResponse` adds `total_delete` (#4674) | | `POST /api/sync/negotiate` payload | New `restore_unlisted` and `emulators` fields to re-offer unlisted saves and filter by emulator (#5091) | | Save sync baselines | `POST /api/saves` and `PUT /api/saves/{id}` take a `content_hash` query param; `POST /api/saves/{id}/downloaded` takes `content_hash` in the body (#4768) | | Save/state uploads validated | `emulator` must be a single folder name (max length enforced); file names over 255 bytes return 400; uploads to a hidden ROM return 404 (#4851, [#4997]) | | `SaveSchema` / `StateSchema` annotations | New `is_favorite` and `labels`; masked to defaults on another user's shared save or state (#4714) | | `StateSchema.core` | New field naming the RetroArch core that wrote the state (#4862) | | ⚠️ `PUT /api/roms/{id}` rename errors | Renaming onto an existing file returns 409 (was 500); an invalid or overlong `fs_name` returns 400 before any metadata fetch (#4997) | | `PUT /api/roms/{id}` manual metadata | `raw_manual_metadata` is validated, 422 when malformed; `ManualMetadata` gains `alternative_names` (#5111) | | `POST /api/roms/{id}/convert-to-folder` | Also returns 409 when an `.m3u` beside the file lists it; upload and walkthrough routes reject the same case (#5076) | | `GET /api/roms` relevance ordering | Empty `order_by` with a search term ranks by relevance on every database; `char_index` is empty under relevance order (#4996) | | `GET/HEAD /api/roms/{id}/content/{file_name}` | New `?format=` for converted single-file downloads (202 + `Retry-After` while converting, 406 when unavailable) (#4999) | | `GET /api/roms/{id}/content/{file_name}` | New `purpose=download\|play` query param, recorded in the audit log (#4748) | | `DetailedRomSchema.download_formats` | Formats the caller can request with `?format=` (#5000) | | `is_easyrpg_game` on ROM schemas | New boolean on `SimpleRomSchema` and `DetailedRomSchema` (#5077) | | Pinned media | `RomUserSchema.pinned_media` and `RomUserData.pinned_media` (ordered media keys, null for default) (#4764) | | ROM metadata fields | LaunchBox box art `box2d_*`/`box3d_*` (url and path), `video_path` now nullable (#5129); SS `physical_disc`/`physical_extra_discs` (#4885); SS and Hasheous `dump_regions/languages/tags` (#4740); RA `hash_match` (#4816) | | `PlatformSchema` | New computed `abbreviation` and `alternative_names` (#5107) | | `MusicTrackSchema.file_name` | New field (#5079) | | `DeviceSchema.capabilities` | New `capabilities` map on device schema and on `POST`/`PUT /api/devices` payloads (#4835) | | `GET /api/heartbeat` | Adds `EMULATION.DISABLE_EASYRPG` (#5077), `NOTIFICATIONS` (#4735), `DEVICE_INSTALL` (#4835), `CONVERTO` (#4999) | | `GET /api/config` | Adds `CONVERTO` and `CONVERTO_LIBRARY_TARGETS` (#4999) | | `POST /api/tasks/run/{task_name}` | Returns 409 when a single-instance task is already queued or running; `TaskInfo` adds `destructive` (#5000) | | `POST /api/reset-password` | Enforces the password policy with 400, leaving the reset link usable (#5106) | | OIDC callback | A provider error redirects to `/login?bypass_autologin=true` instead of 500 (#4746); an account linked to a different provider identity gets 403 (#4892) | | `POST /api/activity/heartbeat` | Returns 404 for a ROM hidden from the caller (#5117) | | ⚠️ `activity:update` socket event | Sent only to users who can see the ROM, instead of broadcast to every client (#5121) | | ⚠️ `scan` socket event payload | Validated strictly (`ScanPayload`, unknown keys rejected); invalid options, no scan worker, or a scan in flight emit `scan:done_ko` (#5014) | | `scan:scanning_rom` | Payload adds `is_new` (#4867) | | `sync:conflict` | Payload adds `rom_name` (#4552) | | Socket session revocation | Logging out or revoking a session disconnects the sockets it opened (#4732) | | Streaming session routes | Control routes take `container` and `claimed_at` query params; launch socket payloads add `claimed_at`, `core`, `core_tier`, import `refusals`; `StreamingContainerSchema` adds `supports_live_states`, `import_kinds`, `state_core`; 409 bodies use `ContainerBusyDetail` (#4631, [#4691], [#4796], [#4859], [#4862]) | #### Notifications | Method | Path | Description | | --- | --- | --- | | GET | `/api/notifications` | List the caller's notifications, newest first (#4732) | | POST | `/api/notifications` | Send a notification to yourself, or to other users with `users.write` (#4732) | | POST | `/api/notifications/read` | Mark the given notifications (or all) read (#4732) | | DELETE | `/api/notifications/{notification_id}` | Dismiss one notification (#4732) | | DELETE | `/api/notifications` | Dismiss all notifications (#4732) | #### Notification channels | Method | Path | Description | | --- | --- | --- | | GET | `/api/notification-channels` | List the caller's channels (Apprise, webhook, email) (#4735) | | POST | `/api/notification-channels` | Create a channel; email addresses get a confirmation code (#4735) | | PATCH | `/api/notification-channels/{channel_id}` | Update a channel (#4735) | | DELETE | `/api/notification-channels/{channel_id}` | Delete a channel (#4735) | | POST | `/api/notification-channels/{channel_id}/test` | Send a test notification now (#4735) | | POST | `/api/notification-channels/{channel_id}/confirm` | Confirm an email channel with its code (#4735) | | POST | `/api/notification-channels/{channel_id}/resend-code` | Resend the email confirmation code (#4735) | | GET | `/api/notification-channels/apprise-services` | List Apprise services and their fields (admin) (#4769) | | POST | `/api/notification-channels/apprise-services/parse` | Parse a service or Apprise URL into fields (admin) (#4769) | #### Audit events | Method | Path | Description | | --- | --- | --- | | GET | `/api/audit-events` | Paginated audit log, filterable by actor, action, category, target, time range and search; admins with `users.read` see everyone (#4748) | #### Device installs | Method | Path | Description | | --- | --- | --- | | GET | `/api/devices/online` | IDs of the caller's devices with an open `/devices` socket (#4835) | | POST | `/api/devices/{device_id}/installs` | Queue a ROM install on a device (#4835) | | GET | `/api/devices/{device_id}/installs` | List the device's pending and taken install requests (#4835) | | POST | `/api/devices/{device_id}/installs/claim` | Device claims its pending requests (device-bound token only) (#4835) | | PUT | `/api/devices/{device_id}/installs/{request_id}` | Device reports `done`, `already_installed` or `failed` (#4835) | | DELETE | `/api/devices/{device_id}/installs/{request_id}` | Cancel a pending or taken request (#4835) | | GET | `/api/roms/{id}/installs` | The caller's open install requests for a ROM (#4835) | #### RetroArch Cloud Sync (WebDAV) | Method | Path | Description | | --- | --- | --- | | OPTIONS | `/api/sync/retroarch/{path}` | Advertise DAV support (#3904) | | PROPFIND | `/api/sync/retroarch/{path}` | Read-only browsing of `roms/`, `saves/` and `states/` (#3904) | | GET, HEAD | `/api/sync/retroarch/{path}` | Serve the manifest or a save/state file (#3904) | | PUT | `/api/sync/retroarch/{path}` | Upload a save/state, matched to a ROM by file name (#3904) | | DELETE, MOVE | `/api/sync/retroarch/{path}` | Remove a save/state the client dropped (#3904) | | MKCOL, LOCK, UNLOCK | `/api/sync/retroarch/{path}` | Accepted no-ops for WebDAV clients; uses HTTP Basic with a 401 challenge (#3904) | #### Saves and states | Method | Path | Description | | --- | --- | --- | | PUT | `/api/saves/{id}/favorite` | Set `is_favorite` on a save (#4714) | | PUT | `/api/saves/{id}/labels` | Replace a save's labels (#4714) | | PUT | `/api/saves/{id}/file-name` | Rename a save file and its screenshot (#4749) | | PUT | `/api/states/{id}/favorite` | Set `is_favorite` on a state (#4714) | | PUT | `/api/states/{id}/labels` | Replace a state's labels (#4714) | | PUT | `/api/states/{id}/file-name` | Rename a state file and its screenshot (#4749) | #### Collections | Method | Path | Description | | --- | --- | --- | | PUT | `/api/collections/{id}/visibility` | Share or unshare a collection (owner only) (#4749) | | PUT | `/api/collections/smart/{id}/visibility` | Share or unshare a smart collection (owner only) (#4749) | #### ROMs, tasks and config | Method | Path | Description | | --- | --- | --- | | POST | `/api/tasks/scan` | Queue a library scan with a `ScanPayload` body; 202, 409 if one is in flight, 503 with no worker (#5014) | | GET | `/api/roms/{id}/easyrpg/{path}` | Serve the EasyRPG `index.json` and game/RTP files for RPG Maker 2000/2003 games (#5077) | | PUT | `/api/config/converto_settings` | Update download conversion settings (#4999) | #### Socket.IO events | Method | Path | Description | | --- | --- | --- | | Server to client | `notifications:new` / `notifications:read` / `notifications:dismissed` | Notification created, marked read, or dismissed for the user (#4732) | | Server to client | `install:updated` | An install request changed, sent to the owner's tabs (#4835) | | Server to client | `install:queued` / `install:cancelled` | Sent to a device on the new `/devices` namespace (device-bound client token required) (#4835) | | Server to client | `activity:refresh` | Signals clients to refetch the activity list (#5125) |

Highlights

Notifications

RomM now keeps a notification inbox for each user. Scans and tasks that finish or fail, a role change, or someone ending your stream all leave a notification that is pushed to every open tab and still waits for you the next time you open RomM. Admins can send their own from the Send tab on the Notifications page, and any API client can post one through POST /api/notifications. [#4732]

Each user can also forward their notifications to channels, filtered by level and topic. A RomM webhook sends a JSON payload (signed with HMAC-SHA256 when you give it a secret), and email works once the SMTP_* variables are set, which also sends password reset links by email. Admins can additionally pick any of the services Apprise supports, such as Discord, Telegram, Slack, ntfy, Gotify or Matrix, and fill in that service's fields or paste its URL. [#4735] [#4769]

Screenshot 2026-10-06 at 6 52 34 AM

RetroArch Cloud Sync

RomM is now a Cloud Sync target for RetroArch. Point RetroArch's Cloud Sync at https://<your-romm>/api/sync/retroarch/ (the trailing slash matters) and sign in with your RomM username and password (setup guide). Saves and states sync straight into your library, web player states show up in RetroArch's numbered load slots, and RetroArch registers itself as one of your devices on its first sync. [#3904]

The config, thumbnails and system file categories sync too, stored per user. PSP save folders are bundled into one save per folder; if a folder's title matches no ROM, map its serial with SYNC_RETROARCH_PSP_SERIAL_MAP. Generic WebDAV clients can browse the same path read-only. [#3931]

Browser saves sync, and installs on your devices

All five in-browser players (EmulatorJS, EasyRPG, js-dos, PICO-8 and Ruffle) now sync saves through device sync. Each browser profile registers as a device, named like "Firefox on macOS", and negotiates its saves with the server when a game launches, so a save made in one browser is waiting in another and a conflicting copy is archived instead of overwritten. A new Devices page at /devices lets you rename your devices, turn sync off for one, or remove it. [#5091]

You can also send a game to a device from the web UI. "Install on device" in a game's menu adds it to the download queue of the devices you pick, and the device downloads it the next time it's online. Only devices whose app reports it accepts installs are listed. Turn the feature off with DEVICE_INSTALL_ENABLED=false, and keep platforms out of it with DEVICE_INSTALL_EXCLUDED_PLATFORM_SLUGS. [#4835]

Screenshot 2026-10-06 at 6 53 03 AM

Parental controls

Give a permission group an age limit, and its members only see games rated for that age or younger. Each user can replace their group's limit, and a "Hide unrated games" switch hides games no rating covers. Admins are never limited. Set them in the group and user dialogs under Administration. [#5081] [#5083]

A game's age comes from the strictest of its IGDB, ScreenScraper, LaunchBox (ESRB) and Steam ratings, and a manual rating list replaces the providers' ratings. The limit applies everywhere a game can show up: the gallery, search, collections, stats, the Jukebox and feeds. See Parental controls for the details.

Screenshot 2026-10-06 at 6 53 46 AM

Audit log

RomM now records who did what: downloads and player launches, play sessions, uploads and edits, collection changes, scans and tasks, and security events like sign-ins, failed sign-ins and permission changes. Admins read it in the new Events tab under Settings › Logs, filtered by user, category and date, with a search over names and IP addresses. Events are kept for 90 days, set by AUDIT_LOG_RETENTION_DAYS (0 keeps them forever). See Audit log for everything that's recorded. [#4748]

Screenshot 2026-10-06 at 6 54 34 AM

Library conversion with rom-converto

Pick a storage format per platform, and the new "Convert library" task converts every matched game on it in place, so a game keeps its saves and collections. Only lossless conversions are offered, and because the task deletes the originals it asks you to type a confirmation first. Set ROM_CONVERTO_ENABLED=true and choose formats under the new Conversion settings page, or in config.yml (setup guide). [#4999]

With download_conversion_enabled on, the game page's More menu offers "Download as" in any format the file can be converted to, and clients can ask for one with ?format= on the download URL. [#5000]

:::yaml
converto:
  download_conversion_enabled: true
  platform_formats:
    psx: chd
    ngc: rvz

RPG Maker 2000/2003 in the browser

RPG Maker 2000 and 2003 games on the rpg-maker platform now play in the browser on the EasyRPG web player. The free EasyRPG RTP ships with it, so games that rely on the RTP start, though some assets (mostly in battles) are missing from it. Games need to be extracted folders, since the web player can't read archives. Disable it with DISABLE_EASYRPG. [#5077]

Screenshot 2026-10-06 at 6 56 37 AM

Search now matches the alternative titles your metadata providers know about, including localized names, so a Japanese title finds the game listed under its English name and the other way around. You can add your own in the new "Alternative titles" field of the Edit dialog, such as "ACNH" or a fan translation's name. Already matched IGDB and Hasheous games pick up their regional titles on their next metadata refresh. [#5111]

The Search page also orders results by relevance until you pick a sort, with exact title matches first. [#4995] [#4996]

Minor changes

Fixes

Other changes

New Contributors

Full Changelog: https://github.com/rommapp/romm/compare/5.3.1...5.4.0

Source: README.md, updated 2026-10-10