# How RomM Synchronizes Save States Across Devices: A Technical Deep Dive

> Discover how RomM synchronizes save states across devices. Learn about its technical approach using device sync records and junction tables to manage save file versions.

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

---

**RomM synchronizes save states across devices by maintaining a per-device sync record in a junction table called `DeviceSaveSync`, which tracks when each device last uploaded or downloaded a specific save file, enabling clients to determine which version is current and whether a save is being tracked on a particular device.**

RomM is an open-source game library manager that enables players to maintain consistent game progress across multiple devices. Understanding how RomM synchronizes save states across devices reveals a sophisticated tracking system that uses device-specific metadata to prevent conflicts and ensure every client knows which save version is authoritative.

## The Core Architecture

The synchronization system in RomM (as implemented in `rommapp/romm`) centers on three integrated components that work together to track save state ownership across devices.

### DeviceSaveSync Model

At the heart of the system lies the **`DeviceSaveSync`** model defined in [`backend/models/device_save_sync.py`](https://github.com/rommapp/romm/blob/main/backend/models/device_save_sync.py). This SQLAlchemy table serves as a junction between devices and saves, storing four critical fields:

- `device_id`: A foreign key string linking to the devices table
- `save_id`: An integer foreign key referencing the specific save file
- `last_synced_at`: A timezone-aware timestamp recording the last synchronization
- `is_untracked`: A boolean flag indicating whether the device should ignore this save

```python

# backend/models/device_save_sync.py

class DeviceSaveSync(BaseModel):
    __tablename__ = "device_save_sync"
    device_id: Mapped[str] = mapped_column(
        String(255), ForeignKey("devices.id", ondelete="CASCADE"), primary_key=True,
    )
    save_id: Mapped[int] = mapped_column(
        ForeignKey("saves.id", ondelete="CASCADE"), primary_key=True,
    )
    last_synced_at: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True))
    is_untracked: Mapped[bool] = mapped_column(Boolean, default=False)

```

### Database Handler Layer

The **`DBDeviceSaveSyncHandler`** in [`backend/handler/database/device_save_sync_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/database/device_save_sync_handler.py) provides the data-access layer for sync operations. This handler exposes methods to fetch, upsert, mark as untracked, and delete sync records. The `upsert_sync` method is particularly critical, as it handles both creating new sync relationships and updating existing ones with the current timestamp:

```python

# backend/handler/database/device_save_sync_handler.py

def upsert_sync(self, device_id: str, save_id: int, synced_at: datetime | None = None):
    now = synced_at or datetime.now(timezone.utc)
    existing = session.scalar(
        select(DeviceSaveSync).filter_by(device_id=device_id, save_id=save_id).limit(1)
    )
    if existing:
        session.execute(
            update(DeviceSaveSync)
            .where(DeviceSaveSync.device_id == device_id,
                   DeviceSaveSync.save_id == save_id)
            .values(last_synced_at=now, is_untracked=False)
        )
        existing.last_synced_at = now
        existing.is_untracked = False
        return existing
    else:
        sync = DeviceSaveSync(
            device_id=device_id,
            save_id=save_id,
            last_synced_at=now,
            is_untracked=False,
        )
        session.add(sync)
        session.flush()
        return sync

```

### Save Endpoints Integration

The save endpoints in [`backend/endpoints/saves.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/saves.py) orchestrate the synchronization logic. Functions like `add_save`, `download_save`, `confirm_download`, and `update_save` invoke the handler to record sync state, while `_build_save_schema` and `_syncs_for_save` enrich API responses with per-device sync data.

## The Sync Lifecycle

RomM processes save state synchronization through a five-stage lifecycle that ensures every device maintains an accurate view of save file ownership.

### 1. Upload and Update Operations

When a user uploads a new save or updates an existing one, the `add_save` or `update_save` function executes after the file is stored on disk. The endpoint calls `db_device_save_sync_handler.upsert_sync()` with the current device ID and save ID, setting `last_synced_at` to the save's `updated_at` timestamp and ensuring `is_untracked` is set to `False`.

### 2. Optimistic Download Syncing

During the `download_save` operation, RomM performs an optimistic sync. If the request includes a `device_id` and the download succeeds, the endpoint immediately calls `upsert_sync` to record that the requesting device now possesses the latest version. This prevents race conditions where a device might download an outdated save immediately after another client uploads a newer one.

### 3. Explicit Download Confirmation

For scenarios requiring guaranteed synchronization, RomM provides the `confirm_download` endpoint (POST `/saves/{id}/downloaded`). This endpoint forces a sync entry regardless of the download method, ensuring the server knows the client successfully retrieved the save file. This is particularly useful for mobile clients or unreliable network conditions.

### 4. Tracking Control

Users can selectively stop or resume tracking saves on specific devices through the `untrack_save` and `track_save` endpoints. These invoke `DBDeviceSaveSyncHandler.set_untracked`, which toggles the `is_untracked` flag without deleting the historical sync record. This allows devices to ignore specific saves while retaining the ability to re-enable tracking later.

### 5. Reading Sync State

Whenever the API returns save data, the `_syncs_for_save` helper calls `DBDeviceSaveSyncHandler.get_syncs_for_saves` to retrieve all device relationships for the requested saves. The endpoint transforms these database rows into `DeviceSyncSchema` entries and attaches them to the `SaveSchema` payload. Clients receive a complete synchronization map showing which devices own the save, when they last synced, and whether the save is current.

```typescript
// Frontend response structure
{
  id: 42,
  file_name: "MyGame.srm",
  device_syncs: [
    {
      device_id: "device-abc",
      device_name: "Laptop",
      last_synced_at: "2024-07-05T12:34:56Z",
      is_untracked: false,
      is_current: true,
    }
  ]
}

```

## Client-Side Synchronization Logic

The frontend consumes the sync data to present users with clear device status indicators. When fetching saves via `/api/saves` with a `device_id` parameter, the client receives the `device_syncs` array containing tuples of synchronization records and device names. This enables the UI to display badges indicating "last synced 5 minutes ago" or "untracked on this device" without requiring additional API calls.

## Summary

RomM's approach to synchronizing save states across devices relies on a robust three-tier architecture:

- **`DeviceSaveSync` model** in [`backend/models/device_save_sync.py`](https://github.com/rommapp/romm/blob/main/backend/models/device_save_sync.py) stores the junction table linking devices to save files with timestamps and tracking flags
- **`DBDeviceSaveSyncHandler`** in [`backend/handler/database/device_save_sync_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/database/device_save_sync_handler.py) provides atomic operations for creating, updating, and querying sync records via `upsert_sync` and `set_untracked`
- **Save endpoints** in [`backend/endpoints/saves.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/saves.py) orchestrate the sync lifecycle during uploads, downloads, confirmations, and API responses through functions like `confirm_download` and `_build_save_schema`

This design ensures every device maintains an accurate view of save file ownership while giving users granular control over which saves appear on which devices.

## Frequently Asked Questions

### How does RomM determine which device has the latest save version?

RomM compares the `last_synced_at` timestamps across all `DeviceSaveSync` records for a given save ID. The device with the most recent timestamp is considered to have the current version, and the `_build_save_schema` function sets `is_current: true` on that device's sync entry in the API response. Clients can then display synchronization status without downloading the actual file.

### What happens when I mark a save as untracked on a device?

When you call the `/saves/{id}/untrack` endpoint, the `untrack_save` function invokes `DBDeviceSaveSyncHandler.set_untracked`, which sets the `is_untracked` flag to `True` for that device-save combination. The record remains in the database for historical purposes, but the client will hide the save from the device's local view. You can reverse this by calling the `/track` endpoint, which clears the flag.

### Can multiple devices upload conflicting save files?

Yes, multiple devices can upload saves for the same game, but RomM handles this through the sync metadata rather than file merging. Each upload creates or updates a `DeviceSaveSync` row with the current timestamp. The next time clients fetch the save list, they see all device versions with their respective sync times, allowing users to choose which version to download or manually resolve conflicts.

### How does the sync system handle offline devices?

The sync system uses timestamp-based tracking rather than real-time presence detection. When an offline device comes back online and downloads a save, it either triggers the optimistic sync in `download_save` or explicitly calls `confirm_download` to record the new state. The `last_synced_at` field ensures that even after periods of disconnection, the system can accurately determine which device holds the most recent save state.