### Index --- title: Introduction description: Your beautiful, powerful, self-hosted ROM manager and player hide: - toc --- RomM logo RomM word type /// caption Welcome to the **RomM Project**, the premier self-hosted, open source ROM manager. ///
Website · Demo · Discord
RomM (ROM Manager) lets you scan, enrich, organise, and play your game collection from a clean web UI, with metadata from IGDB, ScreenScraper, MobyGames, RetroAchievements, Hasheous, LaunchBox, and more. In-browser play via EmulatorJS and Ruffle, companion apps for Android, handhelds, and desktop, and a first-class multi-user experience with OIDC SSO. ## Where do you want to go?
- :material-rocket-launch-outline: **I'm new, get me running** *** 15 minute Docker Compose walkthrough to a working instance. [Quick Start →](getting-started/quick-start.md) - :material-cog-outline: **I'm running RomM for my family/friends** *** Users, roles, OIDC, scheduled tasks, backups, reverse-proxy recipes. [Administration →](administration/index.md) - :material-controller-classic-outline: **I just want to play** *** Library, collections, saves & states, ROM Patcher, Netplay. [Using RomM →](using/index.md) - :material-api: **I'm building on top of RomM** *** API reference, WebSockets, device sync protocol, client tokens. [Developers →](developers/index.md)
## Philosophy RomM is built for its users, not for shareholders: self-hosted, open-source, no tracking, no upsells. The core app is licensed under [GNU AGPLv3](https://choosealicense.com/licenses/agpl-3.0/), and other projects in the umbrella use permissive licenses ([GPLv3](https://choosealicense.com/licenses/gpl-3.0/) for software, [CC0](https://choosealicense.com/licenses/cc0-1.0/) for documentation). **RomM is and will always be free and open-source software.** ## Community Join us on Discord to ask questions, share your setup, request features, or help other users. Code, issues, and releases live on [GitHub](https://github.com/rommapp/romm).
Join our Discord
--- ### About/Brand Guidelines --- title: Brand Guidelines description: How to use the name, logo, and colors --- # Brand Guidelines In this context, "RomM", "The RomM Project", "the project", "we", "us", and "our" refer to the RomM project and organization. ## The logo
RomM logo

The logo should always be used in its standard colors:
Color Hex
#371f69 #371f69
#553e98 #553e98
#ede5f8 #ede5f8
#bea4e1 #bea4e1
#e6c7a7 #e6c7a7
#e1a38d #e1a38d
## Do these things - Use our logo to link to any page or site operated by the project. - Use our logo in a blog post or news article about the project. - Use our logo to inform others that your project integrates with RomM. - Always use our logo in the colors provided. - Always use our name in a way that makes clear you are not affiliated with the project. If you're building something that integrates with RomM and would like to use/remix the logo, **please reach out first** via [Discord](https://discord.gg/romm). We'd love to hear about it. ## Please don't - Use our name or logo in any way that suggests you are us, are endorsed by us, or are part of the project. - Use our name or logo in a way that implies partnership, sponsorship, or endorsement. - Use our name or logo as the name or logo for your project, product, service, social media account, company, or website. - Use our name or logo to promote, advertise, or sell any private business, closed-source software, commercial product, or paid service. ## Downloadables The logo assets live at [rommapp/romm/tree/master/frontend/assets/](https://github.com/rommapp/romm/tree/master/frontend/assets/): - `isotipo.svg`/`isotipo.png`: the mark (circular logo) - `logotipo.svg`/`logotipo.png`: the wordmark - `social_preview.png`: GitHub social preview ## Questions Ask on [Discord](https://discord.gg/romm) or open an issue. --- ### About/Credits --- title: Credits description: The humans, projects, and services that make RomM possible. --- # Credits This project exists because a lot of people contributed code, designs, translations, ideas, and, most importantly, running bug reports back to the project. Thanks to every one of them. ## Contributors [Every contributor](https://github.com/rommapp/romm/graphs/contributors) to [rommapp/romm](https://github.com/rommapp/romm), plus contributors to the surrounding repos: - [rommapp/argosy-launcher](https://github.com/rommapp/argosy-launcher/graphs/contributors) - [rommapp/grout](https://github.com/rommapp/grout/graphs/contributors) - [rommapp/playnite-plugin](https://github.com/rommapp/playnite-plugin/graphs/contributors) - [rommapp/docs](https://github.com/rommapp/docs/graphs/contributors) ## Translators The 19 locales exist because individual community members took the time to translate the UI. See each locale folder's commit history at [rommapp/romm/tree/master/frontend/src/locales](https://github.com/rommapp/romm/tree/master/frontend/src/locales) for per-locale credit. ## Community apps Built by the community, not the RomM team. Full list in the [Community section in the RomM README](https://github.com/rommapp/romm/#community). ## Financial supporters Donors via [Open Collective](https://opencollective.com/romm) make continued development possible. The project wouldn't exist without you. Thank you! ❤️ ## Upstream projects This stack stands on an enormous amount of open-source work. In rough order of "how visible they are to users": ### In-browser emulation - [EmulatorJS](https://emulatorjs.org/) - [Ruffle](https://ruffle.rs/) - [dosbox-pure](https://github.com/schellingb/dosbox-pure) ### Metadata sources - [IGDB](https://www.igdb.com/) - [ScreenScraper](https://screenscraper.fr/) - [MobyGames](https://www.mobygames.com/) - [RetroAchievements](https://retroachievements.org/) - [SteamGridDB](https://www.steamgriddb.com/) - [Hasheous](https://hasheous.org/) - [Playmatch](https://github.com/RetroRealm/playmatch) - [LaunchBox Games Database](https://gamesdb.launchbox-app.com/) - [TheGamesDB](https://thegamesdb.net/) - [Flashpoint Archive](https://flashpointproject.github.io/flashpoint-database/) - [HowLongToBeat](https://howlongtobeat.com/) - [Libretro](https://www.libretro.com/) ### Backend stack - [FastAPI](https://fastapi.tiangolo.com/) + [Starlette](https://www.starlette.io/) - [SQLAlchemy](https://www.sqlalchemy.org/) + [Alembic](https://alembic.sqlalchemy.org/) - [RQ](https://python-rq.org/) - [MariaDB](https://mariadb.org/)/[PostgreSQL](https://www.postgresql.org/)/[MySQL](https://www.mysql.com/) - [Redis](https://redis.io/)/[Valkey](https://valkey.io/) - [nginx](https://nginx.org/) with [`mod_zip`](https://github.com/evanmiller/mod_zip) - [uv](https://docs.astral.sh/uv/) ### Frontend stack - [Vue 3](https://vuejs.org/) - [Vuetify](https://vuetifyjs.com/) - [Pinia](https://pinia.vuejs.org/) - [Vite](https://vitejs.dev/) - [socket.io](https://socket.io/) - [vue-i18n](https://vue-i18n.intlify.dev/) - [rom-patcher-js](https://www.marcrobledo.com/RomPatcher.js/) - [vite-plugin-pwa](https://vite-pwa-org.netlify.app/) ### Docs stack - [MkDocs](https://www.mkdocs.org/) + [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) - [mike](https://github.com/jimporter/mike) for docs versioning - [asciinema](https://asciinema.org/) for terminal recordings ## Missing? Open a PR against this page. Credit is cheap, and we want to give it where it's due. ## See also - [License](license.md) - [Contributing](../developers/contributing.md) --- ### About/Faqs --- title: FAQs description: Answers to the questions users most often ask --- # FAQs ## What is RomM? RomM (ROM Manager) allows you to scan your library, pull metadata, browse a clean UI, play in the browser, sync to handhelds, and run it all on your own hardware. !!! note "RomM is server software you run on a homelab box or NAS" It is not a desktop app, and there's no `.exe` or `.appimage`. You'll need basic Linux + Docker skills to set it up. **Games and copyrighted material aren't provided.** ## Is it free? Yes! Licensed under [AGPL-3.0](license.md), the core will always be free. Other repos under our umbrella use equally permissive licenses, and there's no tracking or upsells. ## How does it compare to [X other manager]? The emphasis here is self-hosted + multi-user + in-browser-play + the companion-app ecosystem. If those matter, try it, but if you just want a local Windows app that scans a folder, tools like LaunchBox may fit better. ## Do I need metadata API keys? It runs without any but games won't match against a metadata source, so you won't get covers, descriptions, or ratings. ## Is RomM legal? The software is legal but what you put in it depends on your jurisdiction. **We don't ship ROMs or firmware, don't help you find them, and can't give legal advice.** ## Can I run RomM on [X]? Probably! Supported deployment paths: - Docker Compose (Linux, macOS, Windows with WSL2) - Unraid/Synology/TrueNAS SCALE - Kubernetes See [Install & Deploy](../install/index.md). Not supported: - Bare metal without containers (undocumented but _should_ work) - TrueNAS CORE (FreeBSD) - Windows without WSL ## How much RAM/CPU does it need? - **Minimum** (small library, 1 user): 512 MB RAM, any modest CPU - **Comfortable** (thousands of ROMs, a few users, occasional scans): 2 GB RAM, 2 cores Heaviest CPU usage is during scans, as hashing and network-bound metadata calls may cause spikes. ## How do I update? ```sh docker compose pull rommapp/romm docker compose up -d romm ``` Read the [upstream release notes](https://github.com/rommapp/romm/releases) before minor or major version upgrades. ## Why can't I see a specific platform? The platform folder name probably doesn't match a known slug. Check [Supported Platforms](../platforms/supported-platforms.md), and fix it by either renaming the folder or updating the [`system.platforms`](../reference/configuration-file.md#systemplatforms) binding in `config.yml`. ## Why are my ROMs unmatched after a scan? Most common reasons: - No metadata providers configured - Filename too generic (no tags, unusual naming) - Wrong platform detection Full troubleshooting steps can be found in [Scanning Troubleshooting](../troubleshooting/scanning.md). ## My scan finds platforms but no games inside them This is almost always a mount-depth issue. The scanner expects the _parent_ of your `roms/` folder mounted to `/romm/library`, not the `roms/` folder itself. If your files live at `/opt/romm/library/roms/gbc/game.gbc`, mount `/opt/romm/library` to `/romm/library`, then re-scan. See [Folder Structure](../getting-started/folder-structure.md) and [Scanning Troubleshooting](../troubleshooting/scanning.md) for the full layout and common mount mistakes. ## Why is my metadata wrong or incomplete? Metadata isn't owned, only pulled from third parties like IGDB and ScreenScraper. If a field is missing or wrong, the fix has to happen upstream on the provider's site. Cross-check against another provider if one is consistently off for your library. ## Why am I getting a "Configuration file not Mounted!" error? A `config.yml` is read from `/config` at startup. If the mount/file is missing or unreadable, startup bails with this error (see [Configuration File](../reference/configuration-file.md) for the schema and an example you can drop in). ## Can I play PS3 or newer consoles in the browser? **No.** In-browser emulation handles 4th-gen and earlier systems well. Saturn, PS1, and N64 are hit-or-miss. PSP and newer are mostly unplayable. Browser play is a bonus, as this is a library manager first, and standalone emulators remain the right tool for modern systems (see [Supported Platforms](../platforms/supported-platforms.md) for the current list). ## Why is browser emulation laggy or not loading? A few common causes: - **Browser**: use a Chromium-based browser (Chrome, Edge, Brave). Firefox and Safari might run EmulatorJS poorly. - **HTTPS**: PSP and DOS cores require the site served over `https://`. Accessing by IP won't work, so you need a [Reverse Proxy](../install/reverse-proxy.md) with TLS. - **Hardware**: EmulatorJS runs entirely in the browser, so CPU-heavy cores need a capable (modern) browser. Full troubleshooting steps can be found in [In-Browser Play](../troubleshooting/in-browser-play.md). ## Can I share my library with friends? Add them as users via the invite flow, then either: - Expose your instance publicly behind a [Reverse Proxy](../install/reverse-proxy.md) with TLS. - Keep it private and share access over a VPN or Tailscale. See [Invitations & Registration](../administration/invitations-and-registration.md). ## Can guests browse without an account? Absolutely, just set `KIOSK_MODE=true` in your environment variables and anonymous visitors get read-only access (see [Authentication → Kiosk mode](../administration/authentication.md#kiosk-mode)). ## How do I back up? `mysqldump` the DB + rsync the `/romm/assets` and `/romm/config` volumes nightly. Full procedure and test-restore protocol in [Backup & Restore](../install/backup-and-restore.md). ## Can I use RomM without the internet? You need internet for: - First-time scan with metadata providers (they're online APIs) - Pulling the Docker image on install or upgrade - OIDC login (if you use a cloud IdP) After the initial setup, browsing and playing can work offline. In-browser play downloads the emulator bundle on first launch, then caches it. ## Why is scan X slow? Several possibilities, in rough order of likelihood: 1. Hashing large files on spinning disks. 2. Metadata providers rate-limiting (mostly ScreenScraper). 3. Many files on a network mount with high latency. [Scanning Troubleshooting → Hash calculations are slow](../troubleshooting/scanning.md#hash-calculations-are-slow). ## When will [feature X] be added? We don't give ETAs on individual features. Open (or upvote) an issue at [rommapp/romm](https://github.com/rommapp/romm/issues) to make the interest visible. ## I found a bug, or I need help For bugs, open an issue at [rommapp/romm](https://github.com/rommapp/romm/issues). For questions, ask in `#romm-support` on [Discord](https://discord.gg/romm). Either way, include: - RomM version - Deployment (Docker Compose/Unraid/K8s/etc.) - Your `docker-compose.yml` with credentials and API keys redacted - Container logs (`docker logs romm`) - Exact reproduction steps ## Who runs RomM? A small team of maintainers plus a chunk of active community contributors. Support the project via [Open Collective](https://opencollective.com/romm) if you'd like! ## Where's can I find you? - **Discord**: [discord.gg/romm](https://discord.gg/romm) - **GitHub**: [rommapp/romm](https://github.com/rommapp/romm) - **Website**: [romm.app](https://romm.app/) - **Demo**: [demo.romm.app](https://demo.romm.app/) ## See also - [Glossary](../reference/glossary.md) if the vocabulary is new - [Troubleshooting](../troubleshooting/index.md) if something's broken --- ### About/License --- title: License description: How RomM is licensed, and what that means for you. --- # License ## Core app: AGPLv3 The main [RomM application](https://github.com/rommapp/romm) is licensed under the [GNU Affero General Public License v3.0](https://choosealicense.com/licenses/AGPLv3/). In short, AGPLv3 says: - **You can** use RomM privately, commercially, or at any scale. - **You can** modify the source code however you want. - **You must** make your modifications available under AGPLv3 if you distribute them _or_ run them as a network-accessible service for others. - **You must** preserve copyright and license notices. If you host a modified RomM and other people use it over the network, you owe them the source. This prevents the "hosted SaaS fork without upstream contributions" failure mode that's common with plain GPL. For most self-hosters this has zero practical effect, since you're running an unmodified version for yourself and the license doesn't constrain you. For companies thinking about shipping a modified RomM commercially, understand the AGPL obligations first. Full license text: [rommapp/romm/blob/master/LICENSE](https://github.com/rommapp/romm/blob/master/LICENSE). ## Companion apps: varies The RomM umbrella hosts several projects under different licenses: | Project | License | | --------------------------------------------------------------------- | ------- | | [rommapp/romm](https://github.com/rommapp/romm) | AGPLv3 | | [rommapp/argosy-launcher](https://github.com/rommapp/argosy-launcher) | AGPLv3 | | [rommapp/grout](https://github.com/rommapp/grout) | MIT | | [rommapp/playnite-plugin](https://github.com/rommapp/playnite-plugin) | AGPLv3 | | [rommapp/docs](https://github.com/rommapp/docs) (what you're reading) | CC0 | Companion repos use more permissive licenses AGPLv3 or MIT because they're smaller, more-replaceable, and don't host the library. The AGPL network-service clause doesn't offer the same protection benefits there. ## Third-party components Several third-party components ship with their own licenses: [EmulatorJS](https://emulatorjs.org/), [Ruffle](https://ruffle.rs/), Vue, FastAPI, and a long list of smaller dependencies. Their licenses apply to their respective code, and none of them override AGPLv3 on the RomM code itself. View the full list via `uv tree` in the backend and `npm ls` in the frontend. Redistribution respects each upstream's terms. ## FAQ ### Can I use RomM at work/for my company's gaming night? Yes. AGPL doesn't restrict private or commercial use. ### Can I fork RomM and relicense my fork? No. AGPL is a strong copyleft, so forks remain AGPL, and while you can add your own changes under AGPL you can't relicense the original code. ### Can I charge money for RomM-as-a-service? In the spirit of the project, we'd prefer if you didn't charge for access to your instance. That being said, it is permitted under our license, but you owe your users the source, so you can't run a modified closed-source hosted RomM for paying customers. We also ask that you make a donation to the project if you do run a paid service, as it helps us cover our costs. ### Can I sell ROMs through RomM? No, as the sale of ROMs is not permitted in most jurisdictions. ### Is there a commercial/dual-license option? Please reach out to us if you're interested in commercial licensing. We may be open to dual-licensing arrangements for certain use cases! ## See also - [Credits](credits.md) - [AGPLv3 overview](https://choosealicense.com/licenses/agpl-3.0/) - Full license text: [LICENSE](https://github.com/rommapp/romm/blob/master/LICENSE) --- ### Administration/Oidc/Authelia --- title: OIDC with Authelia description: Wire up SSO to Authelia --- # OIDC with Authelia [Authelia](https://www.authelia.com/) is a lightweight open-source authentication and authorisation server with two-factor auth and SSO. Before starting, read the [OIDC Setup overview](index.md), as it covers the RomM-side settings common to every provider. ## 1. Prerequisites Authelia installed and running, with its OIDC provider enabled: - [Authelia getting started](https://www.authelia.com/integration/prologue/get-started/) - [OIDC provider configuration](https://www.authelia.com/configuration/identity-providers/openid-connect/provider/) ## 2. Add a claims policy In Authelia's `configuration.yml` under `identity_providers.oidc.claims_policies`, add a policy that emits the claims RomM needs (name it whatever you like): ```yaml identity_providers: oidc: claims_policies: with_email: id_token: - email - email_verified - groups - alt_emails - preferred_username - name ``` Background on why this is needed: [Authelia claims parameter restoration](https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter). ## 3. Register the RomM client Under `identity_providers.oidc.clients`, add: ```yaml identity_providers: oidc: clients: - client_id: "" # see note below client_name: "RomM" client_secret: "$pbkdf2-sha512$" # see note below public: false authorization_policy: "two_factor" # or one_factor grant_types: - authorization_code redirect_uris: - "https://demo.romm.app/api/oauth/openid" claims_policy: "with_email" # must match the policy name above scopes: - openid - email - profile - groups userinfo_signed_response_alg: "none" token_endpoint_auth_method: "client_secret_basic" ``` To generate IDs and secrets, see [Authelia's FAQ](https://www.authelia.com/integration/openid-connect/frequently-asked-questions/#how-do-i-generate-a-client-identifier-or-client-secret). The full client schema is available in the [Authelia clients reference](https://www.authelia.com/configuration/identity-providers/openid-connect/clients/). ## 4. Configure RomM In the `romm` service environment: ```yaml environment: - OIDC_ENABLED=true - OIDC_PROVIDER=authelia - OIDC_CLIENT_ID= - OIDC_CLIENT_SECRET= - OIDC_REDIRECT_URI=https://demo.romm.app/api/oauth/openid - OIDC_SERVER_APPLICATION_URL=https://auth.example.com - ROMM_BASE_URL=https://demo.romm.app ``` `OIDC_REDIRECT_URI` must match what you put in `redirect_uris` exactly (scheme, host, path, no trailing slash). For role mapping from Authelia groups, see [OIDC Setup → Role mapping](index.md#role-mapping). ## 5. Set your email In RomM → **Profile** → set your email to exactly the same address Authelia has for you. RomM matches OIDC users to existing accounts by email. ## 6. Test Restart, navigate to `/login` and click the **Login with OIDC** button. You're redirected to Authelia → authenticate → bounced back and signed in! If it doesn't work, head to [Authentication Troubleshooting](../../troubleshooting/authentication.md). --- ### Administration/Oidc/Authentik --- title: OIDC with Authentik description: Wire up SSO to Authentik --- # OIDC with Authentik [Authentik](https://goauthentik.io/) is a full-featured open-source IdP with MFA, flows, and a sizeable audit/admin surface. Before starting, read the [OIDC Setup overview](index.md), as it covers the RomM-side settings common to every provider. ## 1. Prerequisites Authentik installed and running via their [install guide](https://docs.goauthentik.io/docs/install-config/install/docker-compose). Log in as admin and open **Admin Interface**. ## 2. Create a property mapping (Authentik 2025.10+) !!! important "Authentik 2025.10 breaking change" In version 2025.10, Authentik changed the default of `email_verified` from `true` to `false`. A verified email is required, so without this property mapping, authentication silently fails. In **Customization → Property Mappings → Create → Scope Mapping**: - **Name**: `RomM Email Verification` - **Scope name**: `email` - **Expression**: ```py return { "email": user.email, "email_verified": True, } ``` Click **Create**. Upstream reference: [Authentik scope mappings](https://version-2025-10.goauthentik.io/add-secure-apps/providers/property-mappings/#scope-mappings-with-oauth2). ## 3. Create a provider **Admin → Providers → Create**. Choose **OAuth2/OpenID Provider**. Configure: - **Name**: `RomM OIDC Provider` - **Authorization flow**: implicit consent - **Redirect URIs**: `https://demo.romm.app/api/oauth/openid` - **Scopes**: Under "Advanced protocol settings", move the property mapping you created above from "Available Scopes" to "Selected Scopes". You'll also need to make sure any existing mappings of `email` or `email_verified` are disabled. Authentik has an `email` mapping by default, so make sure to check for this and remove it if it's present. Copy the generated **Client ID** and **Client Secret**. You'll use them as `OIDC_CLIENT_ID`/`OIDC_CLIENT_SECRET` on the app side. Click **Create**. ## 4. Register the application **Admin → Applications → Create**. - **Name**: `RomM` - **Slug**: `romm` - **Provider**: the `RomM OIDC Provider` you just made Click **Create**. ## 5. Configure ```yaml environment: - OIDC_ENABLED=true - OIDC_PROVIDER=authentik - OIDC_CLIENT_ID= - OIDC_CLIENT_SECRET= - OIDC_REDIRECT_URI=https://demo.romm.app/api/oauth/openid - OIDC_SERVER_APPLICATION_URL=https://auth.example.com/application/o/romm - ROMM_BASE_URL=https://demo.romm.app ``` Note that `OIDC_SERVER_APPLICATION_URL` points at the per-application URL (`/application/o/`), not the Authentik root. For role mapping from Authentik groups, see [OIDC Setup → Role mapping](index.md#role-mapping). ## 6. Set your email In **Profile** → set your email to exactly the same address Authentik has for you. ## 7. Test Restart, navigate to `/login` and click the **Login with OIDC** button. You're redirected to Authentik → authenticate → bounced back and signed in! If it doesn't work, head to [Authentication Troubleshooting](../../troubleshooting/authentication.md). --- ### Administration/Oidc/Index --- title: OIDC Setup description: Wire up to an OpenID Connect provider for SSO and centralised user management --- # OIDC Setup OpenID Connect (OIDC) lets users sign in through an external identity provider: Authelia, Authentik, Keycloak, PocketID, Zitadel, Okta, Auth0, VoidAuth, or anything standards-compliant. Single sign-on across your homelab, no app-specific password to manage, centralised MFA, and map OIDC groups/claims to roles. !!! note "OIDC is optional" The local user system works fine without OIDC. Enable OIDC when you already run an IdP and want auth to follow suit, or when you want to unify user management across multiple apps. ## How it works 1. User clicks the OIDC login button on `/login`. 2. They're redirected to your provider. 3. They authenticate (password, passkey, MFA, whatever your provider enforces). 4. Provider redirects back to `{ROMM_BASE_URL}/api/oauth/openid` with an authorisation code. 5. The code is exchanged for an ID token, the user's email and role claims are read, and either a matching local user is created on the fly (unless you've [turned off registration](#auto-provisioning)), or an existing one is logged in. ## Provider guides Pick your provider and follow the step-by-step instructions. They all end with the same set of app-side env vars. The guides just differ on how to register the app and where to find the client ID/secret. - [Authelia](authelia.md) - [Authentik](authentik.md) - [Keycloak](keycloak.md) - [PocketID](pocketid.md) - [Zitadel](zitadel.md) - [VoidAuth](voidauth.md) Not listed? Most standards-compliant OIDC providers work: Okta, Auth0, Google Workspace, Microsoft Entra, etc. Use one of the above as a template and consult your provider's docs for the registration side. ## Minimum config Whichever provider you pick, set these in the `romm` service's environment: ```yaml environment: - OIDC_ENABLED=true - OIDC_PROVIDER= - OIDC_CLIENT_ID= - OIDC_CLIENT_SECRET= - OIDC_SERVER_APPLICATION_URL=https://auth.example.com - OIDC_REDIRECT_URI=https://demo.romm.app/api/oauth/openid - ROMM_BASE_URL=https://demo.romm.app # must match your reverse-proxy URL ``` `OIDC_REDIRECT_URI` must exactly match what you register at the provider (same scheme, host, path, no trailing slash). ## Auto-provisioning By default, the first successful OIDC login for an email that has no matching account **creates** a local account automatically. To require accounts to exist beforehand (so only pre-provisioned users can sign in via OIDC), turn registration off: ```yaml environment: - OIDC_ALLOW_REGISTRATION=false # default: true ``` With it disabled, an unknown user is rejected at login instead of getting a fresh account. New accounts land in the [default permission group](../users-and-roles.md#permission-groups) unless a role claim maps them to Admin. ## Role mapping RomM has only two roles: **User** and **Admin** (see [Users & Roles](../users-and-roles.md#roles)). New OIDC users are provisioned as **Users**. To let your IdP promote someone to **Admin** based on group membership, set: ```yaml environment: - OIDC_CLAIM_ROLES=groups # which claim to read - OIDC_ROLE_ADMIN=romm-admin,platform-admins # group values → Admin ``` On every login, the claim named by `OIDC_CLAIM_ROLES` is read (often `groups`, sometimes `realm_access.roles` on Keycloak, check your provider's token output). If a value matches `OIDC_ROLE_ADMIN`, the user becomes an Admin. Roles are re-evaluated on **every login**, so demoting someone on the IdP side takes effect the next time they sign in. !!! warning "Once `OIDC_CLAIM_ROLES` is set, users must match a mapped group" As soon as `OIDC_CLAIM_ROLES` is configured, RomM expects every user to match at least one mapped role group. A user whose claim matches **none** of the configured groups is rejected at login with: ```json {"detail":"User has not been granted any roles for this application."} ``` To let non-admin users in, map their group to the **User** role with `OIDC_ROLE_VIEWER` (or `OIDC_ROLE_EDITOR`): ```yaml environment: - OIDC_CLAIM_ROLES=groups - OIDC_ROLE_ADMIN=romm-admin,platform-admins # → Admin - OIDC_ROLE_VIEWER=platform-users # non-admins → User (grants access) ``` `OIDC_ROLE_VIEWER` and `OIDC_ROLE_EDITOR` no longer map to distinct roles — matching users all resolve to **User** — but they're still how you grant those users access when role claims are enabled. Point them at a group that all your non-admin users belong to. Use [permission groups](../users-and-roles.md#permission-groups) for finer-grained access; only `OIDC_ROLE_ADMIN` changes the role. If you *don't* set `OIDC_CLAIM_ROLES` at all, role mapping is skipped entirely and everyone is provisioned as a **User** in the default permission group. ## Autologin To bypass the login page entirely and redirect straight to the IdP: ```yaml environment: - OIDC_AUTOLOGIN=true ``` Useful when you want this to feel like a native part of your SSO stack. Combine with `DISABLE_USERPASS_LOGIN=true` to lock out local accounts entirely. !!! warning "Keep one local admin" Don't set `DISABLE_USERPASS_LOGIN=true` without first confirming an admin account exists on the IdP side and can log in. If OIDC breaks and you've disabled local login, you're locked out until you fix the container env. ## RP-Initiated Logout When set, hitting "Sign out" in RomM also signs the user out at the IdP: ```yaml environment: - OIDC_RP_INITIATED_LOGOUT=true - OIDC_END_SESSION_ENDPOINT=https://auth.example.com/application/o/end-session/ ``` The endpoint URL is provider-specific, check the per-provider guides or your IdP's docs. ## Username source By default the local part of the email (the bit before `@`) becomes the username, but you can override it with: ```yaml environment: - OIDC_USERNAME_ATTRIBUTE=preferred_username ``` ## Important notes - **Email must match** between OIDC and any existing local account, otherwise OIDC creates a new account alongside the old one. - **HTTPS is required** in production, as OIDC will refuse to redirect to a plain-HTTP `ROMM_BASE_URL`. - Large drift between the RomM host and IdP will lead to **clock skew** and cause ID-token validation to fail. ## Troubleshooting Common failures and fixes live in [Authentication Troubleshooting](../../troubleshooting/authentication.md). Two of the usual suspects: - `redirect_uri_mismatch`: `OIDC_REDIRECT_URI` differs from what's registered at the provider. Even a trailing slash can matter! - User created but not made Admin: check `OIDC_CLAIM_ROLES` points at a claim that actually exists in the token, and that the group values match `OIDC_ROLE_ADMIN` exactly (case-sensitive). --- ### Administration/Oidc/Keycloak --- title: OIDC with Keycloak description: Wire up SSO to Keycloak --- # OIDC with Keycloak [Keycloak](https://www.keycloak.org/) is the heavyweight open-source IAM standard. Before starting, read the [OIDC Setup overview](index.md), as it covers the RomM-side settings common to every provider. ## 1. Prerequisites Keycloak installed and running via their [getting started guide](https://www.keycloak.org/getting-started). Log into the **Admin Console** and either create a new realm for the app or reuse an existing one. ## 2. Add a client In the Admin Console, select your realm → **Clients** → **Create client**. 1. **Client type**: `OpenID Connect`. 2. **Client ID**: `romm` (or something unique). 3. Click **Next**. 4. On the capability page: - Enable **Client authentication** - Leave only **Standard flow** enabled - Click **Next** 5. Set URLs: - **Root URL**: `https://demo.romm.app` - **Valid Redirect URIs**: `https://demo.romm.app/api/oauth/openid` - **Web origins**: `https://demo.romm.app` 6. Save, then head to the **Credentials** tab and copy the **Client Secret**. ## 3. Configure ```yaml environment: - OIDC_ENABLED=true - OIDC_PROVIDER=keycloak - OIDC_CLIENT_ID=romm - OIDC_CLIENT_SECRET= - OIDC_REDIRECT_URI=https://demo.romm.app/api/oauth/openid - OIDC_SERVER_APPLICATION_URL=https://keycloak.example.com/realms/ - ROMM_BASE_URL=https://demo.romm.app ``` `OIDC_SERVER_APPLICATION_URL` must include the realm (`.../realms/`), not just the Keycloak root. ## 4. Set email + verify in Keycloak In **Profile** → set your email to the same address Keycloak has for you. On the Keycloak side, go to **Admin Console → Users** and mark each user's email as **verified**. Users with unverified emails will be rejected on login. ## 5. Test Restart, navigate to `/login` and click the **Login with OIDC** button. You're redirected to Keycloak → authenticate → bounced back and signed in! If a local user already exists with a matching email, they're signed into that account. Otherwise a new account is created as a regular User in the default permission group. If it doesn't work, head to [Authentication Troubleshooting](../../troubleshooting/authentication.md). ## 6. (Optional) Disable local password logins Force users through Keycloak: ```yaml environment: - DISABLE_USERPASS_LOGIN=true ``` !!! warning Before flipping this, confirm at least one Admin account can sign in via OIDC, otherwise a broken OIDC flow locks you out. ## 7. (Optional) Role mapping By default, new OIDC users come in as regular Users. To let Keycloak promote someone to **Admin** based on a role/group: ```yaml environment: - OIDC_CLAIM_ROLES=groups # or realm_access.roles, depending on your token - OIDC_ROLE_ADMIN=romm-admin ``` Configure Keycloak's client to include the role/group claim in the ID token (usually via a **Group Membership** or **Realm Role** client scope mapper). Values in the claim are compared against `OIDC_ROLE_ADMIN` on every login, so demoting in Keycloak takes effect on the user's next sign-in. See [OIDC Setup → Role mapping](index.md#role-mapping) for the generic version. --- ### Administration/Oidc/Pocketid --- title: OIDC with PocketID description: Wire up SSO to PocketID --- # OIDC with PocketID [PocketID](https://github.com/stonith404/pocket-id) is a minimalist OIDC provider that **only** supports passkey authentication, with no passwords. Before starting, read the [OIDC Setup overview](index.md), as it covers the RomM-side settings common to every provider. ## 1. Prerequisites PocketID installed, running, and your admin passkey already registered via their [PocketID setup guide](https://github.com/stonith404/pocket-id#setup). ## 2. Add the client In PocketID admin: 1. **Application Configuration**: make sure **Emails Verified** is ticked as we now require verified emails. 2. Go to **OIDC Client** → **Add OIDC Client**. 3. Fill in: - **Name**: `RomM` - **Callback URLs**: `https://demo.romm.app/api/oauth/openid` 4. **Save**. Stay on this page as the client secret only displays **once**. 5. Copy both the Client ID and Client Secret now. ## 3. Configure RomM ```yaml environment: - OIDC_ENABLED=true - OIDC_PROVIDER=pocket-id - OIDC_CLIENT_ID= - OIDC_CLIENT_SECRET= - OIDC_REDIRECT_URI=https://demo.romm.app/api/oauth/openid - OIDC_SERVER_APPLICATION_URL=https://id.example.com - ROMM_BASE_URL=https://demo.romm.app ``` `OIDC_SERVER_APPLICATION_URL` is the root URL of your PocketID instance. ## 4. Set your email RomM → **Profile** → set your email to exactly the same address PocketID has for you. ## 5. Test Restart, navigate to `/login` and click the **Login with OIDC** button. You're redirected to PocketID → authenticate → bounced back and signed in! If it doesn't work, head to [Authentication Troubleshooting](../../troubleshooting/authentication.md). --- ### Administration/Oidc/Voidauth --- title: OIDC with VoidAuth description: Wire up SSO to VoidAuth --- # OIDC with VoidAuth [VoidAuth](https://voidauth.app/) is an open-source SSO authentication and user management provider that stands guard in front of your self-hosted application. Before starting, read the [OIDC Setup overview](index.md), as it covers the RomM-side settings common to every provider. ## 1. Prerequisites VoidAuth installed and running via their [self-hosted deployment docs](https://voidauth.app/#/?id=quick-start). We'll use the `APP_URL` from the example `compose.yml` in those docs as `https://auth.example.com`. Your RomM instance will be referenced as `https://demo.romm.app`. ## 2. Create a new app Log in as an admin in the VoidAuth web interface. Create a new OIDC app (e.g. `RomM`). - **Name**: `RomM` - **Home Page URL**: `https://demo.romm.app` - **Logo URL**: `https://docs.romm.app/resources/romm/isotipo.png` - **Group**: You could add a group that the user must belong to get access to your RomM instance. If left empty, any user created in your VoidAuth instance will be allowed. - **Skip Consent** and **MFA Required**: These options can be enabled or left disabled as you prefer. - **Client ID**: Generate an ID using the button. - **Auth Method**: `Client Secret Basic` - **Client Secret**: Generate a secret using the button. - **Redirect URLs**: add `https://demo.romm.app/api/oauth/openid` - **Response Types**: check `code` - **Grant Types**: check `authorization_code` and `refresh_token` - **Post Logout URL**: `https://demo.romm.app/` Don't forget to click the `Create` button to validate your app. ## 3. Configure ```yaml environment: - OIDC_ENABLED=true - OIDC_PROVIDER=voidauth - OIDC_CLIENT_ID= - OIDC_CLIENT_SECRET= - OIDC_REDIRECT_URI=https://demo.romm.app/api/oauth/openid - OIDC_SERVER_APPLICATION_URL=https://auth.example.com - OIDC_SERVER_METADATA_URL=https://auth.example.com/oidc/.well-known/openid-configuration - ROMM_BASE_URL=https://demo.romm.app ``` VoidAuth's OIDC discovery URL can be found at the top of your OIDC App page, by clicking on OIDC Endpoints. For role mapping from VoidAuth, see [OIDC Setup → Role mapping](index.md#role-mapping). ## 4. Test Restart, navigate to `/login` and click the **Login with VoidAuth** button. You're redirected to VoidAuth → authenticate → bounced back and signed in! If it doesn't work, head to [Authentication Troubleshooting](../../troubleshooting/authentication.md). --- ### Administration/Oidc/Zitadel --- title: OIDC with Zitadel description: Wire up SSO to Zitadel --- # OIDC with Zitadel [Zitadel](https://zitadel.com/) is an enterprise-grade open-source IAM platform supporting OAuth2, OIDC, SAML, and passwordless. Before starting, read the [OIDC Setup overview](index.md), as it covers the RomM-side settings common to every provider. ## 1. Prerequisites Zitadel installed and running via their [self-hosted deployment docs](https://zitadel.com/docs/self-hosting/deploy/overview). Change the default organization password before you go further! ## 2. Create a project Create a new project (e.g. `RomM`). This holds the client and its auth settings. On the **General** tab, **Check authorization on Authentication** is recommended. If turned off, anyone who can register in Zitadel can sign into RomM (as a regular User). Turn this on if Zitadel registration is open. ### 2.5 (Optional) Grant users to the project If you enabled **Check authorization on Authentication**: 1. **Authorization** tab → **New**. 2. Select user(s) → **Continue**. 3. "No role has been created yet" is fine, just **Save**. 4. The user appears in the authorization list with no roles. ## 3. Create the application On the project's **General** tab, under **Applications**, click **New**. Tick **I'm a pro. Skip this wizard** for the fast path. - **Name**: `RomM` - **Application Type**: `Web` - **Grant Types**: `Authorization Code` - **Response Types**: `Code` - **Authentication Method**: `Basic` - **Redirect URIs**: `https://demo.romm.app/api/oauth/openid` - **Post Logout URIs**: `https://demo.romm.app/` Click **Create**. The **client secret is shown once**, copy it now! ## 4. Enable claims in the ID Token Without this, RomM throws "Email is missing from token" on login. Open the application's **Token Settings** tab → tick **User Info inside ID Token** → **Save**. ## 5. Configure ```yaml environment: - OIDC_ENABLED=true - OIDC_PROVIDER=zitadel - OIDC_CLIENT_ID= - OIDC_CLIENT_SECRET= - OIDC_REDIRECT_URI=https://demo.romm.app/api/oauth/openid - OIDC_SERVER_APPLICATION_URL=https://zitadel.example.com - ROMM_BASE_URL=https://demo.romm.app ``` Zitadel's OIDC discovery URL is at `/.well-known/openid-configuration`, which is handy for debugging. For role mapping from Zitadel, see [OIDC Setup → Role mapping](index.md#role-mapping). ## 6. Set email + Zitadel In RomM → **Profile** → set your email to exactly the same address your Zitadel user has. ## 7. Test Restart, navigate to `/login` and click the **Login with OIDC** button. You're redirected to Zitadel → authenticate → bounced back and signed in! If it doesn't work, head to [Authentication Troubleshooting](../../troubleshooting/authentication.md). --- ### Administration/Authentication --- title: Authentication description: Configure how users sign in. --- # Authentication This page is the **operator-side** authentication reference, the knobs you turn on the server to control how people sign in. The **client-side** reference ("how do I actually authenticate an API call?") is in [API Authentication](../developers/api-authentication.md). Authentication flows RomM supports: - **Username + password** (default): local account, bcrypt-hashed, stored in the DB - **OIDC**: single sign-on via an external IdP - **Client API Tokens**: long-lived per-user tokens for companion apps and scripts - **Device pairing**: short codes for bootstrapping a token onto a handheld - **Kiosk mode**: unauthenticated read-only access, handy for public demos ## Session config Sessions are cookie-based and stored in Redis. Relevant env vars: | Variable | Default | What it controls | | ------------------------------------ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ROMM_AUTH_SECRET_KEY` | _(required)_ | HS256 signing key for session tokens and JWTs. Generate with `openssl rand -hex 32`. **Never rotate this casually**, because it invalidates every active session and every outstanding invite link. | | `ROMM_AUTH_SECRET_KEY_FILE` | | Read the secret from a file (e.g. via Docker secrets) instead of an env var. | | `SESSION_MAX_AGE_SECONDS` | 86400 (24 h) | How long a session cookie lives before the user has to sign in again. | | `OAUTH_ACCESS_TOKEN_EXPIRE_SECONDS` | 900 (15 min) | Short-lived OAuth2 access token TTL. | | `OAUTH_REFRESH_TOKEN_EXPIRE_SECONDS` | 2592000 (30 d) | Refresh token TTL for OAuth2. | | `DISABLE_CSRF_PROTECTION` | `false` | Disable CSRF middleware. Only do this behind a trusted reverse proxy that strips unwanted cross-origin traffic. | ## Local (username + password) Local accounts are created via [invitations, registration, admin setup, or the Setup Wizard](invitations-and-registration.md), and passwords are bcrypt-hashed. **Disable local password login entirely** (force OIDC-only): ```yaml environment: - DISABLE_USERPASS_LOGIN=true ``` !!! warning "Keep a way in" Before setting `DISABLE_USERPASS_LOGIN=true`, confirm that at least one Admin account can sign in via OIDC. If OIDC breaks and you've already disabled local login, your only way in is editing the container env. ### Admin-triggered password reset Until email-based self-serve reset lands, admins set passwords manually for any user. The next login on that account will use the new password but existing sessions remain valid until they expire. ## OIDC See [OIDC Setup](oidc/index.md) for the full walkthrough. One-liner config sketch: ```yaml environment: - OIDC_ENABLED=true - OIDC_PROVIDER=keycloak - OIDC_CLIENT_ID=... - OIDC_CLIENT_SECRET=... - OIDC_SERVER_APPLICATION_URL=https://auth.example.com - OIDC_REDIRECT_URI=https://demo.romm.app/api/oauth/openid ``` When OIDC is configured, an OIDC sign-in option is offered alongside username/password. Set `OIDC_AUTOLOGIN=true` to redirect straight to the IdP without the user having to choose. By default the first login for an unknown email auto-creates an account; set `OIDC_ALLOW_REGISTRATION=false` to allow only pre-provisioned users in (see [OIDC Setup](oidc/index.md) for role mapping and auto-provisioning). ## Client API Tokens For anything long-lived (a companion app, a cron job, a script) use **Client API Tokens** instead of storing a password. Each token: - Belongs to a specific user - Carries a **subset** of that user's scopes (you choose which at creation time) - Has an optional expiry (no expiry = never expires until manually revoked) - Can be "paired" to a device via a short code Each user gets up to 25 active tokens. The API side ("how do I send this thing in a request?") lives in [API Authentication](../developers/api-authentication.md). ## Kiosk mode Grants unauthenticated, read-only access to nearly every GET endpoint. Anyone reaching the instance can browse but only a logged-in admin can write, scan, upload, or manage users. ```yaml environment: - KIOSK_MODE=true ``` Appropriate for: - Shared-terminal demos - Public-facing "display" instances - `demo.romm.app` Authenticated users (when you do sign in) will still see the full UI with all actions available to their role and [permission group](users-and-roles.md#permission-groups). ## Download-endpoint auth bypass ```yaml environment: - DISABLE_DOWNLOAD_ENDPOINT_AUTH=true ``` Skips auth on `GET /api/roms/{id}/content/…` and the firmware download endpoint. Exists so third-party apps that can't carry a bearer header (like dumb emulators loading a ROM by URL) can still pull files. **Only enable this when the public internet can't reach RomM directly**, i.e. there's auth or an IP allowlist at the reverse-proxy layer. Otherwise you've just made your library world-downloadable. ## Revoking access To fully cut a user off, an admin needs to: 1. Revoke all of the user's Client API Tokens. 2. Disable or delete the user account. Disabling preserves the account row (useful for audit/future re-enable). Deletion removes the user record and personal data, with the caveats covered in [Users & Roles → Editing and deleting users](users-and-roles.md#editing-and-deleting-users). --- ### Administration/Firmware Management --- title: Firmware Management description: Upload, associate, and serve BIOS/firmware files for emulation. --- # Firmware Management Many emulated platforms require BIOS or firmware to boot, or to reach a certain level of stability. RomM tracks firmware files **per platform**, stores them on disk, and serves them to in-browser players (EmulatorJS/Ruffle) and companion apps that request them. Firmware is **not** ROM. Keep the two separate: - `/romm/library/roms/`: game ROMs, homebrew, demos, etc. - `/romm/library/bios/`: system firmware !!! important "Legality varies by jurisdiction" RomM does not ship games or firmware, and the team cannot help you obtain BIOS files. Always check your local laws and the emulator's documentation for guidance on what you can legally use. ## Ingesting firmware 1. Put the file in the right `bios/` folder (see [Folder Structure](../getting-started/folder-structure.md)) 2. Run a scan → Firmware is picked up alongside ROMs 3. Navigate to the platform's gallery and click the `CPU` icon in the top left 4. Firmware files will display at the bottom of the page ## Platform-specific firmware Every emulator has its own requirements: which files it needs, specific hashes, naming conventions. The [Supported Platforms](../platforms/supported-platforms.md) table flags which platforms need firmware for EmulatorJS playback. Common examples: | Platform | Typical firmware file | Where to put it | | ----------------- | ------------------------------------------------- | --------------- | | PlayStation (PSX) | `scph1001.bin`, `scph5501.bin`, `scph5502.bin` | `bios/ps/` | | Game Boy Advance | `gba_bios.bin` | `bios/gba/` | | Sega CD | `bios_CD_U.bin`, `bios_CD_E.bin`, `bios_CD_J.bin` | `bios/segacd/` | | Saturn | `saturn_bios.bin`, `mpr-17933.bin` | `bios/saturn/` | | Nintendo DS | `firmware.bin`, `bios9.bin`, `bios7.bin` | `bios/nds/` | File naming matters as emulators look for specific filenames. Double-check against the emulator core's documentation if something won't boot. ## Integration with companion apps Handheld syncers (Grout, Argosy, DeckRommSync) can pull firmware alongside ROMs. They use the same Client API Token auth as for ROMs, so scope the token to include `firmware.read`. ## Backups Firmware files are user-owned data, so back them up! If you're backing up the `/romm/library` volume, firmware is already covered (see [Backup & Restore](../install/backup-and-restore.md)). --- ### Administration/Index --- title: Administration description: Running RomM for yourself and others. --- # Administration Administration is everything you do **as the operator** of a RomM instance: managing accounts, controlling access, configuring metadata sources, scheduling scans, watching the library for changes, monitoring the server, and keeping data safe. The end-user equivalent (how to actually play the games, build collections, upload saves) lives in [Using RomM](../using/index.md). ## Where things live ### Users & access - **[Users & Roles](users-and-roles.md)**: roles, permission groups, per-user overrides, and hidden entities - **[Invitations & Registration](invitations-and-registration.md)**: invite links, public signup, first-user setup - **[Authentication](authentication.md)**: session config, password reset, Client API Tokens for devices - **[OIDC Setup](oidc/index.md)**: Authelia, Authentik, Keycloak, PocketID, Zitadel, VoidAuth, SSO + role mapping ### Content & library - **[Metadata Providers](../getting-started/metadata-providers.md)**: all providers, credentials, priority ordering - **[Scanning & Watcher](scanning-and-watcher.md)**: how scans work, scan modes, filesystem watcher - **[Firmware Management](firmware-management.md)**: BIOS/firmware uploads for emulation ### Operations - **[Scheduled Tasks](scheduled-tasks.md)**: what runs in the background and how to tune it - **[Server Stats](server-stats.md)**: the stats page and what its numbers mean - **[Observability](observability.md)**: logs, Sentry, OpenTelemetry, `/heartbeat` ### Configuration - **[Environment Variables](../reference/environment-variables.md)**: every env var, grouped by area - **[Configuration File](../reference/configuration-file.md)**: the `config.yml` schema ### Keeping data safe - **[Backup & Restore](../install/backup-and-restore.md)**: routine backups, restore drill, host migration --- ### Administration/Invitations And Registration --- title: Invitations & Registration description: Inviting users, the first-user flow, and role assignment. --- # Invitations & Registration There are three ways a new account ends up on a RomM instance: 1. **First-admin setup**: the person who completes the Setup Wizard. 2. **Invite link**: an admin generates a one-shot link carrying a pre-assigned role. 3. **OIDC auto-provisioning**: first login through your IdP creates a matching account, unless you set `OIDC_ALLOW_REGISTRATION=false` (covered in [OIDC Setup](oidc/index.md)). ## First-admin setup When a fresh RomM container starts against an empty database, hitting any page redirects to the **Setup Wizard**. The wizard collects a username, email, and password. The resulting account is **always an Admin**, regardless of any env var. To skip the wizard (e.g. when provisioning via automation and you'll create users through the API), set: ```yaml environment: - DISABLE_SETUP_WIZARD=true ``` You'll then need to create the first admin via the API or by injecting a database row at deploy time, because the UI won't offer a setup flow. ## Invite links The recommended way to add users, because it avoids you ever touching their password. 1. **Administration → Users → Invite.** Pick a role (User or Admin). 2. RomM generates a single-use URL → copy it and send it to the invitee. 3. When they open it, they pick their own username and password. 4. RomM creates the account with the role you chose and logs them straight in. Invite tokens are **single-use** and **time-limited**. Defaults: | Setting | Default | Env var | | ------- | ---------- | ----------------------------- | | Expiry | 600 (10 m) | `INVITE_TOKEN_EXPIRY_SECONDS` | Expired links return a clear error on the `/register` page. Generate a new one from the Users panel. ## Role assignment at sign-up | Sign-up method | Role assigned | | ----------------------- | -------------------------------------------------------------------------------- | | First-user Setup Wizard | Admin (always) | | Invite link | Whatever role the admin picked when generating the link | | OIDC first login | User (in the default permission group), or Admin if mapped via `OIDC_ROLE_ADMIN` | Changing a user's role afterwards is a normal admin action (see [Users & Roles](users-and-roles.md)). ## Password reset Admins can reset passwords manually in **Administration → Users → Edit → New password**. A temporary password will be printed to the container's logs. --- ### Administration/Observability --- title: Observability description: Logs, error tracking and telemetry --- # Observability It's often handy to know what's happening under the hood, especially when debugging a scan or task. the observability stack includes: - **Container logs**: always available, the first stop - **`/api/heartbeat`** endpoint: health + config summary for uptime monitors - **Sentry**: opt-in error tracking with stack traces - **OpenTelemetry**: opt-in distributed tracing + metrics ## Logs ```yaml environment: - LOGLEVEL=INFO # DEBUG | INFO | WARNING | ERROR - FORCE_COLOR=0 # 1 to force colour even when not a TTY - NO_COLOR=1 # 1 to disable colour entirely ``` `INFO` is the default and the sane choice for production. Drop to `DEBUG` only while debugging a specific issue, because `DEBUG` is chatty. ### Reading logs Log lines are prefixed with module + timestamp: ```text INFO: [RomM][scan_handler][2026-04-18 11:37:40] Identified as PlayStation 🎮 ERROR: [RomM][ra_handler][2026-04-18 11:48:55] Invalid RetroAchievements API key WARNING: [RomM][config_manager][2026-04-18 12:01:12] config.yml not found, using defaults ``` Some useful grep commands: ```sh docker logs romm 2>&1 | grep ERROR docker logs romm 2>&1 | grep -iE 'auth|oidc|oauth' docker logs romm 2>&1 | grep -iE 'scan_handler.*Identified' ``` ### In-app logs viewer RomM also tails the server log live in the UI, with a filter by module (scan handler, auth, config manager, etc.), so you can watch a scan or debug an auth issue without shelling into the container. Access is gated on the `logs.read` scope. To turn it off for everyone (admins included), set: ```yaml environment: - DISABLE_LOGS_VIEWER=true ``` This also disables the `GET /api/logs` endpoint that backs it. The container logs above are unaffected. ## `/api/heartbeat` A single-request endpoint to fetch health and config information. Works when not logged in, though some fields only appear for authenticated callers. ```http GET /api/heartbeat ``` Wire this to your uptime monitor. A failure here means that the process is down or the DB/Valkey is unreachable. ```bash # Basic uptime check curl -fsS https://demo.romm.app/api/heartbeat > /dev/null \ && echo "up" \ || echo "down" ``` Per-metadata provider health: ```http GET /api/heartbeat/metadata/[igdb/ss/ra/...] ``` Useful when a scan is matching poorly and you want to know whether a provider is down on their side or misconfigured on yours. ## Sentry Opt-in error tracking: ```yaml environment: - SENTRY_DSN=https://abc123@sentry.example.com/42 ``` What's sent: - Stack traces for unhandled exceptions - Per-request timing on slow endpoints - Redacted URL parameters (secrets stripped) What's not sent: ROM filenames, user credentials, metadata provider API keys. Sensitive parameters are filtered before reporting. ## OpenTelemetry If you're already using OpenTelemetry with your other apps and want unified observability: ```yaml environment: - OTEL_ENABLED=true - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 - OTEL_SERVICE_NAME=romm - OTEL_RESOURCE_ATTRIBUTES=deployment.environment=prod ``` Standard [OTEL env vars](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) apply. The app emits: - **Traces**: HTTP request spans, DB query spans, RQ job spans - **Metrics**: request counts, durations, queue depth, scan progress - **Logs**: structured log correlation with trace IDs Exporters: - OTLP gRPC (default, port `4317`) - OTLP HTTP (port `4318`): set `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`. Send to an OpenTelemetry Collector, then fan out to Tempo/Jaeger/Honeycomb/Datadog/Grafana/whatever you run. ## Task status Live task state is also exposed programmatically: ```http GET /api/tasks/status Authorization: Bearer ``` Returns an array of every scheduled/manual/watcher task with current status (`idle`, `queued`, `running`, `failed`) and last run time. Scrape this into your monitoring to alert on "Folder Scan hasn't run in 48 hours", which usually means RQ workers are dead. ## Anti-patterns - **Don't parse unstructured log lines** for metrics (use OTEL instead) - **Don't log at DEBUG in production** as the volume is real and scans will drown in it - **Don't scrape HTML pages for health checks**; HTML changes between versions while the API endpoint is stable ## Minimum recommended stack - Default `INFO` logs into the container logs → forwarded to Loki/Promtail/whatever you already run - `/api/heartbeat` hit every 60 seconds from Uptime Kuma/Gatus --- ### Administration/Scanning And Watcher --- title: Scanning & Watcher description: Scanning your library, scan modes, and the filesystem watcher --- # Scanning & Watcher RomM keeps its catalogue in sync with your filesystem through three mechanisms: 1. **Manual scans** you trigger through the web UI 2. **Scheduled scans** (default: nightly) run by the task runner 3. **The filesystem watcher** reacting to files landing in or leaving your library All three share the same scan engine and the same set of **scan modes**. ## Scan modes Every scan picks one mode. Modes differ in what they touch, so use the most-targeted mode that accomplishes what you want. | Mode | What it does | When to use | | ----------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | **New Platforms** | Only scans platform folders not already in the DB. | After mounting a new ROM set (very fast). | | **Quick** | Skips files that already exist in the DB, with no metadata refresh. | Default for scheduled runs and the watcher. | | **Unmatched** | Re-runs metadata matching against ROMs currently missing external IDs. | After adding a new metadata provider, or when some titles didn't match on the first scan. | | **Update** | Re-fetches metadata for all already-matched ROMs. | When metadata providers have meaningfully changed (e.g. IGDB restructured). | | **Hashes** | Recalculates CRC/MD5/SHA1 hashes. | After upgrading from a version that didn't hash or when you suspect file corruption. | | **Complete** | Full rescan, recalculating hashes and re-fetching metadata for everything. | Rarely, since it takes a long time. | You can further scope a scan to specific **platforms** and specific **metadata providers**, useful when only one provider has changed (e.g. just enabled Hasheous → Unmatched scan, Hasheous selected, on all platforms). ## Manual scans A manual scan can be scoped to specific platforms and a chosen subset of metadata providers, and hashing can be skipped (helpful on low-power hosts). A running scan survives browser refreshes, and the log streams over a websocket so multiple users can watch the same scan in progress. ## Scheduled scans Configured via env vars (full table in [Scheduled Tasks](scheduled-tasks.md)): | Variable | Default | Purpose | | -------------------- | ----------- | ------------------------------------------------------------------------------------------------------------ | | `SCAN_INTERVAL_CRON` | `0 0 * * *` | Cron expression for the scheduled library scan. Runs a **Quick** scan by default. | | `SCAN_TIMEOUT` | `14400` | Hard cap in seconds: scans that exceed this are killed and logged. | | `SCAN_WORKERS` | `1` | Concurrent worker processes for scanning; leave as auto unless you're tuning. | | `SEVEN_ZIP_TIMEOUT` | `60` | Per-archive timeout in seconds for `.7z` extraction during scan; raise if scanning huge compressed ROM sets. | To disable scheduled scans entirely, either unset the cron or set it to something unreachable (`SCAN_INTERVAL_CRON=0 0 31 2 *`). ## Filesystem watcher The watcher tails your library folder and schedules scans in response to file events (files added, moved, or deleted). It's off by default, so enable it with: ```yaml environment: - ENABLE_RESCAN_ON_FILESYSTEM_CHANGE=true - RESCAN_ON_FILESYSTEM_CHANGE_DELAY=5 # minutes before acting on an event ``` Behaviour: - Watches `/romm/library` (and everything under it) recursively - Debounces bursts of events: the delay (default 10 seconds) lets a large `cp` or `rsync` settle before scanning. - Batches scans intelligently: many events → a single consolidated scan, not one scan per file - Ignores content modifications and metadata-only changes, caring only about files appearing or disappearing (not `chmod`) - Skips OS noise (`.DS_Store`, `Thumbs.db`, `.tmp`, etc.) - If a whole new platform folder appears, switches to a **New Platforms** scan to pick it up cleanly ### When **not** to enable the watcher - **Slow/high-latency filesystems** (SMB mounts, rclone mounts, anything not local disk): the watcher reacts to every event, flaky mounts generate a lot of them, so use scheduled scans instead. - **Libraries under active write load from other tools** (e.g. IGIR constantly tagging files): the watcher will re-scan on every change, at best noisy and at worst a scan loop. ### Watcher vs scheduled scan | | Watcher | Scheduled scan | | ---------------------------- | ----------------------- | ------------------------ | | Latency | Seconds | Up to your cron interval | | CPU cost | Only when files change | Constant cadence | | Works over SMB/NFS | Flakily | Reliably | | Catches renames | Yes | Yes | | Survives a container restart | Yes, re-arms on startup | Yes | You can run both, where the watcher handles day-to-day additions, and the scheduled scan is a safety net. ## What gets excluded Scans respect the `exclude:` tree in [`config.yml`](../reference/configuration-file.md): ```yaml exclude: platforms: - steam # skip entire platform folder roms: single_file: extensions: [nfo, txt, bak] # single files with these exts names: ["*.sample.*"] # Unix glob patterns multi_file: names: [extras] # folder names to skip parts: names: [thumb.png] # files inside multi-file dirs extensions: [nfo] ``` Full schema in [Configuration File](../reference/configuration-file.md). ## Region and language preference Also in `config.yml`: ```yaml scan: priority: region: [us, wor, ss, eu, jp] language: [en, fr] ``` When a metadata provider returns multiple regional variants (Japanese cover, US cover, European cover…), we pick according to this order, and the same goes for localised titles. ## Metadata source priority Who wins when two providers disagree is covered in [Metadata Providers](../getting-started/metadata-providers.md#priority-and-conflict-resolution), though the short version is `scan.priority.metadata` and `scan.priority.artwork` in `config.yml`. ## Troubleshooting Scans that hang, miss files, or match weirdly: [Scanning Troubleshooting](../troubleshooting/scanning.md). --- ### Administration/Scheduled Tasks --- title: Scheduled Tasks description: Runs tasks in the background, reschedule and trigger them on demand --- # Scheduled Tasks RomM runs background work through **RQ** (Redis Queue). Tasks fall into four categories: - **Scheduled**: cron-driven, run on their own - **Watcher**: triggered by filesystem events - **Manual**: user-triggered from the UI or API - **Enqueued**: side effects of user actions (a scan on the `/scan` page, a metadata refresh on a ROM edit, etc.) ## The full table --8<-- "scheduled-tasks.md" ## Configuring cadence Every scheduled task takes a standard 5-field cron expression: - `0 3 * * *`: 3 AM daily - `0 */6 * * *`: every 6 hours, on the hour - `*/30 * * * *`: every 30 minutes - `0 2 * * 0`: 2 AM every Sunday Set the env var and restart the container; the scheduler picks up the new schedule the moment RomM comes back up. ## Enabling a scheduled task Each task has an `ENABLE_*` environment variable, like `ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA=true` which enables the LaunchBox sync. Check the [env var reference](../reference/environment-variables.md) for the full list. Some tasks are enabled by default, others aren't. ## Triggering a task manually ### From the Administration page **Administration → Tasks** shows every task with a "Run" button. Admins (anyone with `tasks.run` scope) can trigger: ### From the API ```http POST /api/tasks/run/{task_name} Authorization: Bearer ``` ## Monitoring tasks - **Live**: Administration → Tasks page shows every task's current status (queued, running, idle, failed). - **API**: `GET /api/tasks/status` for a JSON summary. Wire this to an uptime monitor if you want alerts. - **Logs**: `docker logs romm` → look for `rq.worker` lines. A task that's been "running" for hours is usually a scan that hit `SCAN_TIMEOUT`, and the logs will say so. Tasks that fail leave a stack trace in the container logs, and the RQ `failed` queue retains the last few for inspection. ## Tuning for small hosts On a Raspberry Pi or NAS with 2 GB of RAM and/or a single CPU core: - Raise the cron intervals (daily → weekly) for the nightlies - Set `SCAN_WORKERS=1` to avoid concurrent scan processes - Enable the watcher but raise `RESCAN_ON_FILESYSTEM_CHANGE_DELAY` to 30+ minutes - Disable image conversion if you don't care about WebP (`ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP=false`). --- ### Administration/Server Stats --- title: Server Stats description: "The numbers Mason! What do they mean?" --- # Server Stats **Administration → Server Stats** is an admin-only page that reports what's on disk and in the catalogue. ## What's shown ### Top-line counts | Metric | What it counts | | ---------------- | --------------------------------------------------------------------------------- | | **Platforms** | Every platform RomM has seen at least one ROM for. Deleted platforms don't count. | | **Games** | Total ROM entries. A multi-file game (folder with multiple files) counts as 1. | | **Saves** | User save files across all users. | | **States** | Emulator save states across all users. | | **Screenshots** | User-uploaded screenshots. Provider-fetched screenshots aren't counted here. | | **Size on disk** | Total disk usage of all ROMs, saves, states, and screenshots. | ### Per-platform breakdown Under the summary, it's a table sorted by name, size or game count. For each platform, you can see: - Game count - Size on disk (in bytes and by percentage of total) - Region distribution (how many games tagged USA, Japan, Europe, World, etc.) - Metadata coverage (how many games have metadata from each provider) When you want to know "which platform is eating my disk?" or "which platform has the worst match rate?" ## API The same data is available programmatically: ```http GET /api/stats GET /api/stats?include_platform_stats=true Authorization: Bearer ``` Wire to your monitoring stack via the API rather than scraping the HTML page (see the [API Reference](../developers/api-reference.md)). ## Troubleshooting - **Numbers look stale**: stats are computed on page load, not cached - **Disk sizes look wrong**: if your compose mounts a path that's smaller than the host dataset (e.g. you mounted a sub-directory), it will only see that subset - **"Platform stats couldn't load"**: the DB query timed out on a very large library For anything else, see [Troubleshooting](../troubleshooting/index.md). --- ### Administration/Users And Roles --- title: Users & Roles description: User management, roles, and the permission-group model --- # Users & Roles The first user created during Setup is always an **Admin**, and everyone after that is a regular **User** whose access is governed by the [permission group](#permission-groups) you assign. ## Roles There are only two roles: | Role | Who it's for | Access | | --------- | -------------------------------- | ---------------------------------------------------------------------------------- | | **Admin** | You, and anyone you fully trust. | Admins **bypass permission groups** entirely, including user management and tasks. | | **User** | Everyone else | Whatever their assigned permission group grants, plus any per-user overrides. | ## Permission groups Each User belongs to a **permission group**: a named template of capabilities that you manage in the new UI (**Administration → Permissions**). A group is a **grant matrix** over entity types and actions: | Entity | `read` | `write` | `delete` | | ------------- | ----------------------------- | -------------------------------- | ---------------- | | `platforms` | Browse platforms | Edit/create platforms | Delete platforms | | `roms` | Browse ROMs | Edit ROM metadata | Delete ROMs | | `collections` | Browse collections | Create/edit | Delete | | `firmware` | List firmware | Upload firmware | Delete firmware | | `assets` | View saves/states/screenshots | Upload/replace | Delete | | `devices` | View paired devices | Pair/manage devices | Unpair devices | | `users` | List users | Create/edit users | Delete users | | `tasks` | View task status | Trigger tasks (scan, cleanup, …) | — | | `logs` | View server logs | — | — | Rules of the model: - **A missing grant means denied.** A group only allows what it explicitly lists. - **`own_only`** narrows a grant to entities the user owns. For example, `assets.write` with `own_only` lets a user manage their own saves/states/screenshots but not anyone else's. - **Default group**: exactly one group is marked as the server-wide default and is applied automatically to every new User (invite sign-up, OIDC first login, admin-created accounts without an explicit group). ### Per-user overrides On top of the group, you can **add or revoke individual capabilities** for one user without creating a whole new group: - **Grant** an override to give a user something their group lacks. - **Revoke** an override to take away something their group provides. Use overrides for one-offs ("this one user can also delete ROMs"), and use groups for anything you'd apply to more than one person. ### Hidden entities Beyond allow/deny, you can **hide specific platforms or ROMs** from a user or from an entire group. A hidden entity simply doesn't appear for that principal, regardless of read grants. Firmware visibility isn't hidden directly, as it cascades from the platform it belongs to. ## Creating users Three paths: - **Admin page**: set username, email, password, role, and a permission group. The account is usable immediately. - **Invite link**: when you don't want to handle someone else's password. The admin generates a single-use link, and the recipient picks their own credentials. New Users land in the default permission group. Invite links expire after 600 seconds by default, configurable via [`INVITE_TOKEN_EXPIRY_SECONDS`](../reference/environment-variables.md). - **OIDC**: if you've wired up OIDC, new identities can be provisioned on first login. Whether that happens is controlled by `OIDC_ALLOW_REGISTRATION`, and Admin mapping from OIDC claims is covered in [OIDC Setup](oidc/index.md). ## Editing and deleting users Admins can change any user's role, move them between permission groups, add per-user overrides, reset their password, or delete the account. Role and permission changes take effect on next login. You can't delete the last admin or delete yourself while signed in. Deleting a user keeps their contributions (collections they made public, ROM metadata edits) but removes their personal data from the database (per-ROM ratings, saves, states, play sessions, paired devices, API tokens). It does not delete any files from disk. ## OAuth scopes The permission groups above are the source of truth for the UI. For the **API**, RomM derives a flat set of OAuth **scopes** from a user's effective grants (group + overrides). Each `(entity, action)` grant maps to the scope of the same name, e.g. `roms` + `write` → `roms.write`. Client API Tokens and OIDC sessions carry a **subset** of the owning user's scopes, and every endpoint declares which scopes it requires. The full scope list (grouped by resource): | Resource | Scopes | | ----------- | -------------------------------------------------------------- | | Profile | `me.read`, `me.write` | | ROMs | `roms.read`, `roms.write`, `roms.user.read`, `roms.user.write` | | Platforms | `platforms.read`, `platforms.write` | | Assets | `assets.read`, `assets.write` | | Collections | `collections.read`, `collections.write` | | Firmware | `firmware.read`, `firmware.write` | | Devices | `devices.read`, `devices.write` | | Users | `users.read`, `users.write` | | Tasks | `tasks.run` | | Logs | `logs.read` | ## API tokens (advanced) Each user can issue up to 25 **Client API Tokens**. A token carries a subset of the owning user's scopes (see above), whichever you pick at creation time. Tokens are the right way to authenticate companion apps (Argosy, Grout, Playnite, custom scripts). The pairing flow for devices is covered in [Client API Tokens](../developers/client-api-tokens.md), and the API side is in [API Authentication](../developers/api-authentication.md). --- ### Developers/Api Authentication --- title: API Authentication description: How to authenticate to the API --- # API Authentication The API accepts multiple authentication modes: | Mode | Who it's for | How the credential is carried | | -------------------- | -------------------------------------------------------- | ------------------------------------------------ | | **Session cookie** | Browser UI | `Cookie: session=…` after `POST /api/auth/login` | | **HTTP Basic** | Quick scripts, curl one-liners | `Authorization: Basic ` | | **OAuth2 Bearer** | Automation, CI, third-party apps | `Authorization: Bearer ` | | **Client API Token** | Companion apps (Argosy, Grout, Playnite, custom scripts) | `Authorization: Bearer rmm_` | All of them resolve to the same scope model. See the [OAuth scopes in Users & Roles](../administration/users-and-roles.md#oauth-scopes). A request is allowed if the active identity holds all scopes the endpoint requires. ## Base URL ```text https:///api ``` When the app is behind a reverse proxy (as it should be when hosted in public), that's your public URL. When running locally without a proxy, the container listens on port `80`. ## Session login (browsers) ```http POST /api/auth/login Content-Type: application/x-www-form-urlencoded username=alice&password=s3cret ``` Response sets a `romm_session` cookie, and subsequent requests from the same browser are authenticated automatically. Log out: ```http POST /api/auth/logout ``` For OIDC logins, hitting `/api/auth/logout` also triggers RP-Initiated Logout if your OIDC provider supports it (configured via `OIDC_END_SESSION_ENDPOINT`). ## HTTP Basic ```bash curl -u alice:s3cret https://demo.romm.app/api/roms ``` ```python import requests from requests.auth import HTTPBasicAuth r = requests.get("https://demo.romm.app/api/roms", auth=HTTPBasicAuth("alice", "s3cret")) ``` ## OAuth2 Bearer token The RomM backend implements the OAuth2 password grant, where you exchange credentials for a short-lived access token and a refresh token. Access and refresh token expiry times are configurable via `OAUTH_ACCESS_TOKEN_EXPIRE_SECONDS` and `OAUTH_REFRESH_TOKEN_EXPIRE_SECONDS`. ```http POST /api/token Content-Type: application/x-www-form-urlencoded grant_type=password&username=alice&password=s3cret&scope=roms.read%20roms.write ``` ```json { "access_token": "eyJhbGciOi...", "refresh_token": "eyJhbGciOi...", "token_type": "bearer", "expires": 1800, "refresh_expires": 604800 } ``` Access tokens are HS256-signed JWTs valid for `OAUTH_ACCESS_TOKEN_EXPIRE_SECONDS` seconds. Send them as: ```http Authorization: Bearer eyJhbGciOi... ``` Refresh before expiry: ```http POST /api/token Content-Type: application/x-www-form-urlencoded grant_type=refresh_token&refresh_token=eyJhbGciOi... ``` Request only the scopes you need and RomM will issue a token with the intersection of what you asked for and what the user has. ## Client API tokens (for companion apps) For anything long-lived (a running companion app, a cron job, a CI integration), use **Client API Tokens** instead of OAuth2. They're issued per-user from **Administration → Client API Tokens**, carry a subset of the user's scopes, and don't expire unless you set an expiry. Token format: `rmm_` + 64 hex chars. Use it as a bearer: ```bash curl -H "Authorization: Bearer rmm_abcdef0123456789..." \ https://demo.romm.app/api/roms ``` Each user gets up to 25 active tokens. Tokens can be paired with a device via the [pairing flow](client-api-tokens.md), useful when you don't want to type a long token on a handheld. ## OIDC Users signing in through an OIDC provider get a regular RomM session, same as username/password login. For the API side this means you can't use an OIDC access token directly. Authenticate the user through the browser first (they'll be redirected to the OIDC provider, then back to RomM), then use the resulting session cookie, **or** mint a Client API Token for programmatic use. OIDC provider setup lives in [Administration → OIDC](../administration/oidc/index.md). ## Which scopes do I need? Every endpoint in the [API Reference](api-reference.md) lists its required scopes. The short version: - **Read**-ish endpoints want the matching `*.read` scope. - **Write**-ish endpoints want `*.write`. - **Admin**-ish endpoints want `users.read`, `users.write`, or `tasks.run`. ## Errors | HTTP | Meaning | | ------------------ | ----------------------------------------------------------------------- | | `401 Unauthorized` | No credential, expired credential, bad credential. | | `403 Forbidden` | Authenticated but the identity lacks a required scope. | | `404 Not Found` | The resource doesn't exist, or, for privacy, the identity can't see it. | When debugging a 403, check: 1. The **user's role** in Administration → Users. 2. The **token's scopes** (for OAuth2/Client API Tokens). Scopes are narrower than the user's role by default. 3. The endpoint's scope requirements in the [API Reference](api-reference.md). ## OpenAPI The full machine-readable schema is served at `/openapi.json`. It's the source of truth for generated clients, Postman collections, and the in-docs [API Reference](api-reference.md). ```bash curl https://demo.romm.app/openapi.json > romm-openapi.json ``` See [Consuming OpenAPI](openapi.md) for codegen tips. --- ### Developers/Api Reference --- title: API Reference description: Catalogue of the API. Authoritative interactive docs live on each instance. --- ## Interactive docs Every RomM instance hosts two renderings of its own spec: - **Swagger UI** at `{romm_url}/api/docs`: explore + try endpoints inline - **ReDoc** at `{romm_url}/api/redoc`: cleaner reading layout The raw spec: ```text {romm_url}/openapi.json ``` For code generation, see [Consuming OpenAPI](openapi.md). ## WebSockets REST isn't the only surface. Two socket.io endpoints cover live-update and coordination use cases: [WebSockets](websockets.md). ## Versioning The API follows SemVer along with the rest of RomM: - **Breaking changes only in major versions.** Endpoint removal, required-parameter changes, incompatible response-schema shifts - **Minor versions add** endpoints, optional parameters, optional response fields. - **Patch versions fix** bugs without schema changes. ## See also - [API Authentication](api-authentication.md): auth modes in detail - [Consuming OpenAPI](openapi.md): codegen + schema validation - [WebSockets](websockets.md): socket.io endpoints - [Client API Tokens](client-api-tokens.md): recommended companion-app auth - [Device Sync Protocol](device-sync-protocol.md): sync endpoints in depth --- ### Developers/Architecture --- title: Architecture description: High-level walkthrough of the codebase --- # Architecture What you need to know to find your way around `rommapp/romm` before you start changing things. The exhaustive deep-dives live alongside the code at [`docs/BACKEND_ARCHITECTURE.md`](https://github.com/rommapp/romm/blob/main/docs/BACKEND_ARCHITECTURE.md) and [`docs/FRONTEND_ARCHITECTURE.md`](https://github.com/rommapp/romm/blob/main/docs/FRONTEND_ARCHITECTURE.md). This page is the orientation pass. ## Repo layout ```text rommapp/romm ├── backend/ # Python FastAPI application │ ├── endpoints/ # Route handlers + socket.io under sockets/ │ ├── handler/ # Business logic: database/, metadata/, filesystem/, auth/ │ ├── adapters/services/# External API clients (IGDB, Moby, SS, SGDB, RA, ...) │ ├── models/ # SQLAlchemy ORM │ ├── tasks/ # RQ jobs: scheduled/ and manual/ │ ├── alembic/ # 80+ DB migrations │ └── config/ # Env vars + YAML config.json manager ├── frontend/ # Vue 3 + Vuetify SPA (main UI + alternate UI) │ └── src/ │ ├── views/ # Page-level components │ ├── components/ # ~168 components, organised by feature │ ├── console/ # Alternate UI SPA (own router, layout, input bus) │ ├── stores/ # 18 Pinia stores │ ├── services/ # Axios API modules + socket.io + browser cache │ └── __generated__/# TS types generated from the backend OpenAPI spec ├── docker/ # nginx config, entrypoint, Dockerfiles └── examples/ # Reference docker-compose.yml and config.yml ``` ## Runtime topology A running RomM container hosts several cooperating processes: ```ascii ┌─────────────────────────────────────────────────────────┐ │ docker container │ │ │ │ ┌───────┐ HTTP ┌──────────┐ python ┌──────────┐ │ │ │ nginx │─────────→│ gunicorn │─────────→│ FastAPI │ │ │ │ :8080 │ │ :5000 │ │ backend │──┐ │ └───┬───┘ └──────────┘ └──────────┘ │ SQL │ │ static files (SPA, EmulatorJS, Ruffle) ↓ │ │ ┌──────────┐ │ │ │ MariaDB │ │ ↓ │ (or PG/│ │ /library /assets /resources │ MySQL) │ │ (host bind mounts) └──────────┘ │ │ │ ┌──────────┐ ┌──────────────────────┐ │ │ │ RQ │←───────→│ Valkey │ │ │ │ workers │ jobs │ (embedded or external)│ │ │ └──────────┘ └──────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ## Request lifecycle Every request runs the middleware stack in order, CORS → CSRF → authentication → Valkey-backed session → context vars (aiohttp + httpx clients), before FastAPI dispatches to the endpoint. Handlers do the actual work and Pydantic schemas serialise the response. ## Backend ### Layers The backend follows a fairly conventional layering. Endpoints handle request validation and response serialisation, while the actual business logic lives in handlers organised by concern: `handler/database/` for per-entity CRUD, `handler/metadata/` for provider-specific normalisation, `handler/filesystem/` for I/O, and `handler/auth/` for the multi-method auth backend. Models are SQLAlchemy ORM, and adapters wrap the external APIs. ### Authentication `HybridAuthBackend` walks methods in order of session cookie (looked up in Valkey), HTTP Basic (bcrypt), OAuth2 Bearer JWT (HS256), Client API Token (`rmm_...`, SHA-256 lookup), OIDC, kiosk mode if enabled. Token plaintext is never stored as we hash on creation and compare hashes on every request. ### Metadata providers Each provider has a handler under `handler/metadata/` that normalises responses into a common shape. Priority is configurable in `config.yml` (`scan.priority.metadata`), where first match wins per field, with manual overrides on top. Hashing can be platform-aware: `.chd` files pull their SHA1 values straight from the file header, PICO-8 cartridges (`.p8.png`) get a special-cased extractor, and the RetroAchievements per-platform algorithm runs through `rahasher.py`. Switch and PS3/4/5 skip hashing entirely, since those ROMs aren't reasonably hashable in the first place and have no TOSEC/No-Intro entries. ### Configuration Environment variables (100+ of them, all listed in `env.template`) cover infrastructure concerns, while `config.yml` covers everything to do with the library, scanning, and emulator behaviour. The config is read, validated, and written back through the singleton `ConfigManager`. ### Background jobs RQ workers run scheduled jobs (rescans, Switch TitleDB refresh, LaunchBox refresh, image-to-WebP conversion, RA progress sync, netplay cleanup) and manual tasks (`cleanup_missing_roms`, `cleanup_orphaned_resources`, `sync_folder_scan`). Each scheduled task is gated by an `ENABLE_SCHEDULED_*` env var and tunable via the matching `*_CRON`. Operator-side detail in [Scheduled Tasks](../administration/scheduled-tasks.md). ## Frontend ### Stack The frontend is a Vue 3 SPA written in TypeScriptusing the Composition API and `