# How to Integrate Custom Metadata Adapters with RomM: A Complete Developer Guide

> Learn to integrate custom metadata adapters with RomM. This guide details creating Python classes, implementing methods, registering modules, and enabling adapters for enhanced functionality.

- Repository: [The RomM Project/romm](https://github.com/rommapp/romm)
- Tags: how-to-guide
- Published: 2026-07-05

---

**To integrate custom metadata adapters with RomM, you create a Python class that inherits from `MetadataHandler` in `backend/handler/metadata/`, implement the required `is_enabled()`, `search()`, and `fetch()` methods, register the module in [`__init__.py`](https://github.com/rommapp/romm/blob/main/__init__.py), and enable it via a configuration flag.**

RomM enriches ROM files with metadata by delegating lookups to pluggable **metadata adapters** that live under `backend/handler/metadata/`. Each adapter inherits from the abstract base class `MetadataHandler` defined in [`backend/handler/metadata/base_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/metadata/base_handler.py), allowing the scan workflow in [`backend/watcher.py`](https://github.com/rommapp/romm/blob/main/backend/watcher.py) to discovery and execute them automatically. This modular architecture lets you integrate any third-party game database API without modifying core scan logic.

## Understanding the Metadata Handler Architecture

The foundation of RomM's metadata system rests on the `MetadataHandler` abstract base class. Located in [`backend/handler/metadata/base_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/metadata/base_handler.py), this class defines the contract that every adapter must fulfill.

When [`backend/watcher.py`](https://github.com/rommapp/romm/blob/main/backend/watcher.py) initiates a library scan, it builds a mapping of enabled adapters using `source_mapping = {handler: handler.is_enabled() for handler in METADATA_HANDLERS}`. The watcher then calls each active handler's `search` and `fetch` methods, merging the returned JSON blobs into dedicated columns in the `roms` table (such as `igdb_metadata`, `moby_metadata`, or `ss_metadata`).

The base class provides critical utilities for adapter development:

- **`normalize_search_term`** – Sanitizes ROM names for API queries
- **`strip_sensitive_query_params`** – Removes API keys from logs
- **`find_best_match`** – Fuzzy matches search results against file names

## Creating a Custom Metadata Adapter

### Implement the Handler Class

Create a new file in `backend/handler/metadata/` (for example, [`myprovider_handler.py`](https://github.com/rommapp/romm/blob/main/myprovider_handler.py)) that subclasses `MetadataHandler`. You must implement three core methods:

1. **`is_enabled()`** – Returns `True` when the adapter should participate in scans, typically by reading a configuration flag from [`backend/config/config_manager.py`](https://github.com/rommapp/romm/blob/main/backend/config/config_manager.py)
2. **`search(self, term: str)`** – Queries the external API and returns a list of candidate dictionaries, each containing at least a `name` field
3. **`fetch(self, game_id: str)`** – Retrieves full metadata for a selected game and returns a dictionary matching RomM's internal schema

Here is a minimal implementation:

```python

# backend/handler/metadata/myprovider_handler.py

import httpx
from typing import Any, List

from handler.metadata.base_handler import MetadataHandler, METADATA_FIXTURES_DIR
from logger.logger import log


class MyProviderHandler(MetadataHandler):
    """Fetch game metadata from MyProvider API."""

    @classmethod
    def is_enabled(cls) -> bool:
        # Reads the flag from the config manager – see config_manager.py

        from config.config_manager import ConfigManager

        return ConfigManager.get_bool("ENABLE_MYPROVIDER", default=False)

    async def search(self, term: str) -> List[dict]:
        """Search MyProvider for games matching *term*."""
        normalized = self.normalize_search_term(term)
        url = f"https://api.myprovider.com/v1/search?q={normalized}"
        async with httpx.AsyncClient() as client:
            resp = await client.get(url, timeout=10.0)
            resp.raise_for_status()
            data = resp.json()
        # MyProvider returns a list of objects with ``title`` and ``id`` fields.

        return [{"name": g["title"], "id": g["id"]} for g in data.get("results", [])]

    async def fetch(self, game_id: str) -> dict:
        """Retrieve detailed metadata for *game_id*."""
        url = f"https://api.myprovider.com/v1/games/{game_id}"
        async with httpx.AsyncClient() as client:
            resp = await client.get(url, timeout=15.0)
            resp.raise_for_status()
            raw = resp.json()

        # Map MyProvider fields to RomM’s schema.

        return {
            "summary": raw.get("description"),
            "url_cover": raw.get("cover_url"),
            "url_screenshots": raw.get("screenshots", []),
            "release_year": raw.get("release_year"),
            # Any extra keys are stored as‑is in the JSON column ``myprovider_metadata``.

        }

    async def get_metadata(self, search_term: str) -> dict:
        """High‑level helper used by the scan process."""
        candidates = await self.search(search_term)
        best, score = await self.find_best_match(search_term, [c["name"] for c in candidates])
        if not best:
            log.debug("MyProvider: no match for %s", search_term)
            return {}
        game_id = next(c["id"] for c in candidates if c["name"] == best)
        return await self.fetch(game_id)

```

### Register the Adapter

Add your handler to the package initializer so [`backend/watcher.py`](https://github.com/rommapp/romm/blob/main/backend/watcher.py) can discover it automatically:

```python

# backend/handler/metadata/__init__.py

from .myprovider_handler import MyProviderHandler  # noqa: F401

```

The import pattern follows the convention `from .myprovider_handler import MyProviderHandler`, using the `noqa: F401` comment to suppress linting warnings for unused imports since the registration happens via side-effect.

### Enable via Configuration

Create a boolean flag in your environment configuration to control adapter activation:

```ini

# .env (or .env.example)

ENABLE_MYPROVIDER=true
MYPROVIDER_API_KEY=your_api_key_here

```

The `is_enabled()` method reads this via `ConfigManager.get_bool("ENABLE_MYPROVIDER", default=False)`, ensuring the adapter only runs when explicitly enabled.

## Wiring Into the Scan Pipeline

Once registered and enabled, your adapter integrates automatically into the metadata refresh process. The [`backend/watcher.py`](https://github.com/rommapp/romm/blob/main/backend/watcher.py) file gathers all enabled handlers and executes them during library scans.

When a scan triggers, the watcher:

1. Collects enabled adapters via the `source_mapping` dictionary comprehension
2. Iterates through each ROM file and calls the adapter's `get_metadata` method
3. Merges returned dictionaries into the database (either into a dedicated JSON column or generic metadata fields)

Trigger a rescan to populate your new metadata:

```bash

# From the repository root

uv run python -m backend.watcher

```

Or invoke the API endpoint directly:

```bash
curl -X POST http://localhost:3000/api/tasks/scan-library -H "Authorization: Bearer <admin-token>"

```

## Database Considerations for Custom Fields

If your provider supplies data that warrants dedicated columns rather than generic JSON storage, extend the `Rom` model in [`backend/models/rom.py`](https://github.com/rommapp/romm/blob/main/backend/models/rom.py) with your new fields.

After modifying the model, generate an Alembic migration:

```bash
uv run alembic revision --autogenerate -m "add myprovider columns"

```

This creates a migration script in your `alembic/versions` directory. Apply it with `uv run alembic upgrade head`. The new columns will then be populated automatically during subsequent scans, accessible via the API at `GET /api/roms/{id}` or displayed in the frontend.

## Summary

- **Create a handler module** in `backend/handler/metadata/` that subclasses `MetadataHandler` and implements `is_enabled()`, `search()`, and `fetch()`
- **Leverage base class utilities** like `normalize_search_term()` and `find_best_match()` to handle fuzzy matching and data sanitization
- **Register the adapter** by importing it in [`backend/handler/metadata/__init__.py`](https://github.com/rommapp/romm/blob/main/backend/handler/metadata/__init__.py) so the watcher can discover it automatically
- **Enable via configuration** by adding a boolean flag (e.g., `ENABLE_MYPROVIDER`) that `is_enabled()` reads from [`backend/config/config_manager.py`](https://github.com/rommapp/romm/blob/main/backend/config/config_manager.py)
- **Extend the database schema** if needed by modifying [`backend/models/rom.py`](https://github.com/rommapp/romm/blob/main/backend/models/rom.py) and running Alembic migrations
- **Trigger scans** using either the watcher CLI or the REST API to populate metadata immediately

## Frequently Asked Questions

### What methods must I implement in a custom metadata handler?

You must implement three methods: `is_enabled()` to return a boolean based on configuration flags, `search(term: str)` to return a list of candidate games with `name` fields, and `fetch(game_id: str)` to return a dictionary of metadata matching RomM's schema. The base class provides `get_metadata()` as a high-level wrapper that calls these automatically.

### How does RomM decide which metadata adapter to use?

RomM uses the `source_mapping` dictionary in [`backend/watcher.py`](https://github.com/rommapp/romm/blob/main/backend/watcher.py) to filter handlers where `is_enabled()` returns `True`. Because you register adapters in [`backend/handler/metadata/__init__.py`](https://github.com/rommapp/romm/blob/main/backend/handler/metadata/__init__.py), they are automatically included in `METADATA_HANDLERS` and evaluated during each scan cycle without modifying the watcher code.

### Can I store custom metadata in dedicated database columns?

Yes. If you need structured storage beyond the default JSON columns, modify the `Rom` model in [`backend/models/rom.py`](https://github.com/rommapp/romm/blob/main/backend/models/rom.py) to add your fields, then generate and apply an Alembic migration using `uv run alembic revision --autogenerate`. The scan pipeline will populate these columns alongside the standard metadata fields.

### How do I test my custom adapter without running a full library scan?

You can test your adapter's `search()` and `fetch()` methods directly by instantiating your handler class in a Python REPL or test script. For integration testing, trigger a targeted scan via the API endpoint `POST /api/tasks/scan-library` with specific parameters, or temporarily modify [`watcher.py`](https://github.com/rommapp/romm/blob/main/watcher.py) to scan a single directory while debugging.