## 1. Project Overview & Quickstart (rommapp/romm)

# romm

A beautiful, powerful, self-hosted rom manager and player.

### Repository Details
- **Repository:** [rommapp/romm](https://github.com/rommapp/romm)
- **GitHub Stars:** ⭐ 11,912
- **Primary Language:** Python

*Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.*

## 2. Official Technical Reference & Guides (rommapp/docs)

## File: docs/about/brand-guidelines.md

---
title: Brand Guidelines
---



# Brand Guidelines

In this context, "RomM", "The RomM Project", "the project", "we", "us", and "our" refer to the RomM project and organization.

## The logo



The logo should always be used in its standard colors:





## 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.

---

## File: docs/about/credits.md

---
title: Credits
---

# 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

These apps are built by the community, not the RomM team. The full list is 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, and the project wouldn't exist without you. Thank you! ❤️

## Upstream projects

This stack depends on a large amount of open-source work. In rough order of "how visible they are to users":

### In-browser emulation

- [EmulatorJS](https://emulatorjs.org/)
- [`js-dos`](https://js-dos.com/)
- [DOSBox-X](https://dosbox-x.com/)
- [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)

---

## File: docs/about/faqs.md

---
title: FAQs
---

# 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 matches neither a known slug nor one of the [folder name aliases](../platforms/supported-platforms.md#folder-name-aliases). 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?

RomM doesn't own metadata. It pulls it 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?

Yes. 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. The full procedure and test-restore protocol are 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.

See [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 group of active community contributors. Support the project via [Open Collective](https://opencollective.com/romm) if you'd like!

## Where 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

---

## File: docs/administration/oidc/authelia.md

---
title: OIDC with 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: "<random>" # see note below
              client_name: "RomM"
              client_secret: "$pbkdf2-sha512$<random>" # 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=<the client_id you picked>
    - OIDC_CLIENT_SECRET=<the plaintext 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's **Profile**, set your email to exactly the same address Authelia has for you. RomM matches an existing account by email on its first OIDC login, then by the Authelia user it linked, so a later email change in Authelia carries over.



## 6. Test

Restart, navigate to `/login` and click the **Login with OIDC** button. RomM redirects you to Authelia, and after you authenticate it sends you back signed in.



If it doesn't work, head to [Authentication Troubleshooting](../../troubleshooting/authentication.md).

---

## File: docs/administration/oidc/authentik.md

---
title: OIDC with 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** (see [Authentik scope mappings](https://version-2025-10.goauthentik.io/add-secure-apps/providers/property-mappings/#scope-mappings-with-oauth2) upstream).

## 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". Also disable any existing mappings of `email` or `email_verified`. Authentik ships an `email` mapping by default, so check for it 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=<from Authentik>
    - OIDC_CLIENT_SECRET=<from Authentik>
    - 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/<slug>`), 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. RomM redirects you to Authentik, and after you authenticate it sends you back signed in.



If it doesn't work, head to [Authentication Troubleshooting](../../troubleshooting/authentication.md).

---

## File: docs/administration/oidc/index.md

---
title: OIDC Setup
---

# 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. You get single sign-on across your homelab and centralised MFA, with no app-specific password to manage, and you can map OIDC groups or 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 and the user's email, username and role claims are read. Claims the ID token leaves out are fetched from the provider's UserInfo endpoint.
6. The matching local user is logged in (see [Account matching](#account-matching)), or a new one is created on the fly unless you've [turned off registration](#auto-provisioning).

If the provider returns an error, or the code exchange or ID token fails validation, RomM logs the reason on an `OIDC callback rejected` line and sends the browser to `/login?bypass_autologin=true`, where the bypass flag keeps [autologin](#autologin) from looping straight back to the provider. A sign-in RomM refuses after that, such as for a disabled account or with registration turned off, returns an error instead of redirecting.

## Provider guides

Pick your provider and follow the step-by-step instructions. Every guide ends with the same set of app-side env vars and differs only in how you register the app and where you find the client ID and 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=<authelia|authentik|keycloak|pocket-id|zitadel|voidauth|generic>
    - OIDC_CLIENT_ID=<from your provider>
    - OIDC_CLIENT_SECRET=<from your provider>
    - 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).

## Private certificate authority

If your provider's HTTPS certificate comes from a private CA, such as a homelab step-ca or an internal Active Directory CA, it can't be verified out of the box. Mount the CA certificate into the container and point `OIDC_TLS_CACERTFILE` at it:

```yaml
environment:
    - OIDC_TLS_CACERTFILE=/romm/config/ca/homelab-root.pem
volumes:
    - ./ca:/romm/config/ca:ro
```

The path can be a single bundle file or a directory, in which case every file in it is read, and PEM, DER and PKCS#7 (`.p7b`) certificates all work. These certificates are trusted alongside the system CAs, so public providers keep working. A path that doesn't exist, or a file with no certificate in it, is logged and skipped.

## 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`, or `realm_access.roles` on Keycloak, so check your provider's ID token or UserInfo response). 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. Only `OIDC_ROLE_ADMIN` changes the role, so use [permission groups](../users-and-roles.md#permission-groups) for finer-grained access.

    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.



## Account matching

The first OIDC login for an existing local account matches it by email, so set the account's email to exactly the address your provider has for the user. That login links the account to the user's identity at the provider (the token's `iss` issuer and `sub` subject). Every later login matches on that identity:

- **Email changes at the provider carry over.** The user still signs into the same account, and RomM stores the new email, unless another account already uses it.
- **Switching providers keeps accounts.** A login from a new issuer matches by email again and relinks the account to the new provider.
- **A new subject for a linked email is refused.** When the same provider sends a different subject for an email that is already linked, RomM rejects the login with a 403 rather than hand the account to someone else. This happens when the provider reassigns the email or recreates the user. See [Authentication Troubleshooting](../../troubleshooting/authentication.md#this-account-is-linked-to-a-different-identity-at-the-provider) to relink it.

## Autologin

To bypass the login page entirely and redirect straight to the IdP, so RomM feels like a native part of your SSO stack:

```yaml
environment:
    - OIDC_AUTOLOGIN=true
```

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, so 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
```

Whatever that attribute holds gets sanitised before it becomes a username to prevent invalid characters from being used. RomM also sends a PKCE challenge on every authorization request, so it's safe to mark PKCE required for the client if your IdP offers that.

## Important notes

- **Email must match** between OIDC and an existing local account on its first OIDC login, otherwise OIDC creates a new account alongside the old one. After that, the account follows the user's identity at the provider (see [Account matching](#account-matching)).
- **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. A trailing slash alone is enough to trigger it.
- User created but not made Admin: check `OIDC_CLAIM_ROLES` points at a claim that actually exists in the ID token or the UserInfo response, and that the group values match `OIDC_ROLE_ADMIN` exactly (case-sensitive).

---

## File: docs/administration/oidc/keycloak.md

---
title: OIDC with 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=<from Keycloak Credentials tab>
    - OIDC_REDIRECT_URI=https://demo.romm.app/api/oauth/openid
    - OIDC_SERVER_APPLICATION_URL=https://keycloak.example.com/realms/<realm-name>
    - ROMM_BASE_URL=https://demo.romm.app
```

`OIDC_SERVER_APPLICATION_URL` must include the realm (`.../realms/<realm-name>`), 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. RomM redirects you to Keycloak, and after you authenticate it sends you back 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).

---

## File: docs/administration/oidc/pocketid.md

---
title: OIDC with 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, because RomM requires 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, because the client secret is displayed only 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=<from PocketID>
    - OIDC_CLIENT_SECRET=<from PocketID>
    - 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

In RomM's **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. RomM redirects you to PocketID, and after you authenticate it sends you back signed in.



If it doesn't work, head to [Authentication Troubleshooting](../../troubleshooting/authentication.md).

---

## File: docs/administration/oidc/voidauth.md

---
title: OIDC with VoidAuth
---

# OIDC with VoidAuth

[VoidAuth](https://voidauth.app/) is an open-source SSO authentication and user management provider that sits in front of your self-hosted applications. 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 in order to 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/`

Click the `Create` button to validate your app.

## 3. Configure

```yaml
environment:
    - OIDC_ENABLED=true
    - OIDC_PROVIDER=voidauth
    - OIDC_CLIENT_ID=<from VoidAuth>
    - OIDC_CLIENT_SECRET=<from VoidAuth>
    - 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. RomM redirects you to VoidAuth, and after you authenticate it sends you back signed in.

If it doesn't work, head to [Authentication Troubleshooting](../../troubleshooting/authentication.md).

---

## File: docs/administration/oidc/zitadel.md

---
title: OIDC with 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**. Zitadel shows the client secret only once, so copy it now.

## 4. Enable claims in the ID Token

RomM fetches claims the ID token leaves out from Zitadel's UserInfo endpoint, and putting them in the ID token avoids that extra request. It also rules out "Email is missing from token" when the UserInfo response doesn't carry the email. On the application's **Token Settings** tab, tick **User Info inside ID Token** and **Save**.

## 5. Configure

```yaml
environment:
    - OIDC_ENABLED=true
    - OIDC_PROVIDER=zitadel
    - OIDC_CLIENT_ID=<from Zitadel>
    - OIDC_CLIENT_SECRET=<from Zitadel>
    - 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 `<OIDC_SERVER_APPLICATION_URL>/.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's **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. RomM redirects you to Zitadel, and after you authenticate it sends you back signed in.

If it doesn't work, head to [Authentication Troubleshooting](../../troubleshooting/authentication.md).

---

## File: docs/administration/authentication.md

---
title: Authentication
---

# Authentication

This page is the operator-side authentication reference: the settings you change 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.

### Password reset

A user who forgot their password can ask for a reset link from the sign-in page (`POST /api/forgot-password`), which works once within 10 minutes. Where it goes depends on the server:

- With [email](email.md) set up, `ROMM_BASE_URL` pointing at a real host, and an email address on the account, the link goes to that address, at most once a minute per user.
- Otherwise, or when the email can't be sent, the link is written to the container log for an admin to pass on.

The response is the same whether or not the username exists, so the form can't be used to find out which accounts exist. Requests for an existing account and completed resets are both recorded in the [audit log](audit-log.md).

Admins can also set any user's password directly, and the new password applies from the next login while existing sessions stay 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

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. It 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).

---

## File: docs/administration/firmware-management.md

---
title: Firmware Management
---

# 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, and firmware is picked up alongside ROMs
3. The platform's firmware is then listed on that platform's page, and offered to the in-browser player

Where `bios/` lives is up to you. The default is `bios/{platform}` at the library root, and you can move it with the [`filesystem.structure.firmware`](../reference/configuration-file.md#filesystemstructure) template.

## Missing firmware

Delete a file from `bios/` and the next scan flags it missing instead of dropping it from the database. Put the file back and the next scan clears the flag. If you're never replacing it, the **Cleanup missing firmware** task deletes every flagged row in one go (see [Scheduled Tasks](scheduled-tasks.md#triggering-a-task-manually)).

## 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 because 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)).