romm Documentation

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

13,957 stars Python Python 4 files ยท ~43,257 tokens #emulation#open-source#retrogaming#rommapp#self-hosted
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy romm on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name romm -p 3000:3000 --restart always mariadb:13.0.2
Configured Services & Ports
โš™๏ธ romm-dev
image: build locally
๐Ÿ”Œ Ports: 3000:3000, 5173:5173, 8443:8443, 5678:5678, ${DEV_PORT:-5000}:5000
โš™๏ธ romm-db-dev
image: mariadb:13.0.2
๐Ÿ”Œ Ports: 3306:3306
โš™๏ธ romm-valkey-dev
image: valkey/valkey:9.0.6
๐Ÿ”Œ Ports: ${REDIS_PORT:-6379}:6379
docker-compose.yml
yaml (docker-compose.yml)
# Please see the full example under examples/docker-compose.example.yml

# Pinned so the default network stays romm_default whatever the checkout
# directory is named; docker-compose.oidc.yml and .streaming.yml join it.
name: romm

services:
  romm-dev:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: romm-dev
    env_file: .env
    environment:
      - REDIS_HOST=romm-valkey-dev
      - DB_HOST=${DB_HOST:-romm-db-dev}
      - ROMM_BASE_PATH=/app/romm
    ports:
      - "3000:3000" # Vite dev server (custom)
      - "5173:5173" # Vite dev server (default)
      - "8443:8443" # HTTPS dev server
      - "5678:5678" # debugpy
      - "${DEV_PORT:-5000}:5000" # Backend API
    volumes:
      - ./backend:/app/backend
      - /app/backend/romm_mock # Empty directory
      - ./frontend:/app/frontend
      - /app/frontend/node_modules # Empty directory
      - /app/frontend/dist # Empty directory
      - ./romm_mock:/app/romm
      - ~/.vite-plugin-mkcert:/app/.vite-plugin-mkcert
      # Read-only, so the backend can report the checked-out branch (see
      # backend/utils/__init__.py get_git_branch) without git write access.
      - ./.git:/app/.git:ro
    depends_on:
      - romm-db-dev
      - romm-valkey-dev
    command: /bin/bash -c "cd /app && bash"
    stdin_open: true
    tty: true

  romm-db-dev:
    image: mariadb:13.0.2
    container_name: romm-db-dev
    restart: unless-stopped
    env_file: .env
    environment:
      - MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWD:-rootpassword}
      - MARIADB_DATABASE=${DB_NAME:-romm}
      - MARIADB_USER=${DB_USER:-romm}
      - MARIADB_PASSWORD=${DB_PASSWD:-romm}
    volumes:
      - romm-db-dev:/var/lib/mysql
    ports:
      - "3306:3306"

  romm-valkey-dev:
    image: valkey/valkey:9.0.6
    container_name: romm-valkey-dev
    restart: unless-stopped
    env_file: .env
    ports:
      - "${REDIS_PORT:-6379}:6379"

volumes:
  romm-db-dev:

1. Project Overview & Quickstart (rommapp/romm)

romm

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

Repository Details

  • Repository: 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 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. 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/:

  • isotipo.svg/isotipo.png: the mark (circular logo)
  • logotipo.svg/logotipo.png: the wordmark
  • social_preview.png: GitHub social preview

Questions

Ask on Discord 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 to rommapp/romm, plus contributors to the surrounding repos:

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

Financial supporters

Donors via Open Collective 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

Metadata sources

Backend stack

Frontend stack

Docs stack

Missing?

Open a PR against this page. Credit is cheap, and we want to give it where it's due.

See also


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

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.

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 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. Check Supported Platforms, and fix it by either renaming the folder or updating the system.platforms 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.

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 and Scanning Troubleshooting 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 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 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 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.

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 with TLS.
  • Keep it private and share access over a VPN or Tailscale.

See Invitations & Registration.

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

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.

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.

When will [feature X] be added?

We don't give ETAs on individual features. Open (or upvote) an issue at rommapp/romm to make the interest visible.

I found a bug, or I need help

For bugs, open an issue at rommapp/romm. For questions, ask in #romm-support on Discord. 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 if you'd like!

Where can I find you?

See also


File: docs/administration/oidc/authelia.md


title: OIDC with Authelia

OIDC with Authelia

Authelia is a lightweight open-source authentication and authorisation server with two-factor auth and SSO. Before starting, read the OIDC Setup overview, as it covers the RomM-side settings common to every provider.

1. Prerequisites

Authelia installed and running, with its OIDC provider enabled:

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.

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. The full client schema is available in the Authelia clients reference.

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.

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.


File: docs/administration/oidc/authentik.md


title: OIDC with Authentik

OIDC with Authentik

Authentik is a full-featured open-source IdP with MFA, flows, and a sizeable audit/admin surface. Before starting, read the OIDC Setup overview, as it covers the RomM-side settings common to every provider.

1. Prerequisites

Authentik installed and running via their install guide.

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

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.


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), or a new one is created on the fly unless you've turned off registration.

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

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 unless a role claim maps them to Admin.

Role mapping

RomM has only two roles: User and Admin (see Users & 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:

text
```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 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).
  • 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. 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 is the heavyweight open-source IAM standard. Before starting, read the OIDC Setup overview, as it covers the RomM-side settings common to every provider.

1. Prerequisites

Keycloak installed and running via their getting started guide.

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.

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 for the generic version).


File: docs/administration/oidc/pocketid.md


title: OIDC with PocketID

OIDC with PocketID

PocketID is a minimalist OIDC provider that only supports passkey authentication, with no passwords. Before starting, read the OIDC Setup overview, 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.

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.


File: docs/administration/oidc/voidauth.md


title: OIDC with VoidAuth

OIDC with VoidAuth

VoidAuth 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, as it covers the RomM-side settings common to every provider.

1. Prerequisites

VoidAuth installed and running via their self-hosted deployment docs. 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.

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.


File: docs/administration/oidc/zitadel.md


title: OIDC with Zitadel

OIDC with Zitadel

Zitadel is an enterprise-grade open-source IAM platform supporting OAuth2, OIDC, SAML, and passwordless. Before starting, read the OIDC Setup overview, as it covers the RomM-side settings common to every provider.

1. Prerequisites

Zitadel installed and running via their self-hosted deployment docs. 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.

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.


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.

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

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

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.

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.


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

Platform-specific firmware

Every emulator has its own requirements: which files it needs, specific hashes, naming conventions. The Supported Platforms 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).