RomM logo

RomM

Beautiful, powerful, self-hosted rom manager and player

Alternative to: antstream arcade


About Versions (122)

5.4.0-alpha.1

2026-10-06

[!CAUTION] Give the first start time 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

variabledefaultdescription
SMTP_HOST-SMTP server; email stays off until this and SMTP_FROM are set
SMTP_PORT587SMTP 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_SECURITYstarttlsstarttls, tls (implicit TLS, usually port 465) or none

Audit log

variabledefaultdescription
AUDIT_LOG_RETENTION_DAYS90Days to keep audit events; 0 keeps them forever

Library conversion

variabledefaultdescription
ROM_CONVERTO_ENABLEDfalseEnable the “Convert library” task and ?format= download conversions
ROM_CONVERTO_TIMEOUT600Seconds per rom-converto operation
ROM_CONVERTO_MAX_CONCURRENCY2Concurrent conversions per web or task worker

Devices and sync

variabledefaultdescription
DEVICE_INSTALL_ENABLEDtrueLet users push a ROM to one of their devices for install
DEVICE_INSTALL_REQUEST_TTL_DAYS2Days an unfinished install request waits for its device; 0 waits forever
DEVICE_INSTALL_EXCLUDED_PLATFORM_SLUGSwin,win3x,win9x,windows-appsPlatforms 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

variabledefaultdescription
ENABLE_SCHEDULED_CLEANUP_NETPLAYtrueClean up empty netplay rooms
SCHEDULED_CLEANUP_NETPLAY_CRON*/30 * * * *When that cleanup runs
ENABLE_SCHEDULED_CLEANUP_UPLOAD_TMPtrueClean up abandoned chunked uploads
SCHEDULED_CLEANUP_UPLOAD_TMP_CRON0 * * * *When that cleanup runs
ENABLE_SCHEDULED_CLEANUP_ZIP_CACHEtrueClean up stale cached ZIP files
SCHEDULED_CLEANUP_ZIP_CACHE_CRON0 4 * * *When that cleanup runs
ENABLE_SCHEDULED_CLEANUP_SYNC_SESSIONStrueFail sync sessions no client completed
SCHEDULED_CLEANUP_SYNC_SESSIONS_CRON23 * * * *When that cleanup runs

General

variabledefaultdescription
⚠️ ROMM_CORS_ALLOWED_ORIGINS-Empty now allows no cross-origin requests; a * allows any origin but never credentials
⚠️ FORWARDED_ALLOW_IPSlocal/loopback addressesProxies trusted to report the client’s address; was *
DISABLE_EASYRPGfalseDisable the EasyRPG player for everyone
SEVEN_ZIP_TIMEOUT180Timeout 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

MethodPathDescription
GET/api/notificationsList the caller’s notifications, newest first (#4732)
POST/api/notificationsSend a notification to yourself, or to other users with users.write (#4732)
POST/api/notifications/readMark the given notifications (or all) read (#4732)
DELETE/api/notifications/{notification_id}Dismiss one notification (#4732)
DELETE/api/notificationsDismiss all notifications (#4732)

Notification channels

MethodPathDescription
GET/api/notification-channelsList the caller’s channels (Apprise, webhook, email) (#4735)
POST/api/notification-channelsCreate 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}/testSend a test notification now (#4735)
POST/api/notification-channels/{channel_id}/confirmConfirm an email channel with its code (#4735)
POST/api/notification-channels/{channel_id}/resend-codeResend the email confirmation code (#4735)
GET/api/notification-channels/apprise-servicesList Apprise services and their fields (admin) (#4769)
POST/api/notification-channels/apprise-services/parseParse a service or Apprise URL into fields (admin) (#4769)

Audit events

MethodPathDescription
GET/api/audit-eventsPaginated audit log, filterable by actor, action, category, target, time range and search; admins with users.read see everyone (#4748)

Device installs

MethodPathDescription
GET/api/devices/onlineIDs of the caller’s devices with an open /devices socket (#4835)
POST/api/devices/{device_id}/installsQueue a ROM install on a device (#4835)
GET/api/devices/{device_id}/installsList the device’s pending and taken install requests (#4835)
POST/api/devices/{device_id}/installs/claimDevice 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}/installsThe caller’s open install requests for a ROM (#4835)

RetroArch Cloud Sync (WebDAV)

MethodPathDescription
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

MethodPathDescription
PUT/api/saves/{id}/favoriteSet is_favorite on a save (#4714)
PUT/api/saves/{id}/labelsReplace a save’s labels (#4714)
PUT/api/saves/{id}/file-nameRename a save file and its screenshot (#4749)
PUT/api/states/{id}/favoriteSet is_favorite on a state (#4714)
PUT/api/states/{id}/labelsReplace a state’s labels (#4714)
PUT/api/states/{id}/file-nameRename a state file and its screenshot (#4749)

Collections

MethodPathDescription
PUT/api/collections/{id}/visibilityShare or unshare a collection (owner only) (#4749)
PUT/api/collections/smart/{id}/visibilityShare or unshare a smart collection (owner only) (#4749)

ROMs, tasks and config

MethodPathDescription
POST/api/tasks/scanQueue 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_settingsUpdate download conversion settings (#4999)

Socket.IO events

MethodPathDescription
Server to clientnotifications:new / notifications:read / notifications:dismissedNotification created, marked read, or dismissed for the user (#4732)
Server to clientinstall:updatedAn install request changed, sent to the owner’s tabs (#4835)
Server to clientinstall:queued / install:cancelledSent to a device on the new /devices namespace (device-bound client token required) (#4835)
Server to clientactivity:refreshSignals 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

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-alpha.1