# RomM Save State Synchronization: How It Tracks Saves Across Devices

> Discover how RomM synchronizes save states across devices. Learn about the junction table that tracks the latest save file versions for seamless cross-device play.

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

---

**RomM implements save state synchronization using a per-device junction table that records the last sync timestamp and tracking status for every save file, enabling clients to determine which device holds the most recent version.**

RomM is an open-source ROM manager designed to organize and stream game libraries across multiple devices. Its save state synchronization system ensures that every client knows whether a save has been uploaded, downloaded, or marked as untracked on a given device, preventing conflicts and data loss across your gaming setup.

## The Architecture of Save State Synchronization

RomM's synchronization architecture centers on three core components working together to maintain a consistent view of save states across devices.

### DeviceSaveSync Model

At the heart of the system is 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 junction table links devices to their saves using a composite primary key:

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

```

The `last_synced_at` field stores the UTC timestamp of the most recent synchronization, while `is_untracked` acts as a flag to exclude specific devices from tracking a particular save.

### Database Handler Layer

The `DBDeviceSaveSyncHandler` class 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 managing sync records. Its primary method, `upsert_sync`, creates or updates sync entries atomically:

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

```

This handler also exposes methods to fetch syncs for saves (`get_syncs_for_saves`) and toggle the tracking status (`set_untracked`), which the API endpoints consume to orchestrate synchronization flows.

## How the Sync Lifecycle Works

The save state synchronization flow follows a strict lifecycle managed by the endpoints in [`backend/endpoints/saves.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/saves.py). Each operation updates the `DeviceSaveSync` table to reflect the current state across devices.

### Upload and Update Operations

When a user uploads a new save or updates an existing one, the `add_save` or `update_save` functions record the synchronization immediately after the file is stored and metadata scanned:

```python

# Called within add_save and update_save in backend/endpoints/saves.py

db_device_save_sync_handler.upsert_sync(
    device_id=device.id,
    save_id=db_save.id,
    synced_at=db_save.updated_at,
)

```

This creates a new sync row or updates the existing one with the current UTC timestamp and sets `is_untracked=False`, indicating that the uploading device now holds the authoritative version.

### Download and Optimistic Sync

During download operations, RomM supports **optimistic synchronization**. When the `download_save` endpoint handles a request that includes a `device_id` parameter, it automatically records the sync assuming the download succeeds:

```python

# Within download_save in backend/endpoints/saves.py

if optimistic_sync and device_id:
    db_device_save_sync_handler.upsert_sync(
        device_id=device_id,
        save_id=save_id,
        synced_at=datetime.now(timezone.utc)
    )

```

This approach ensures that the server immediately reflects that the requesting device has the latest version, reducing the need for separate confirmation calls in stable network conditions.

### Explicit Confirmation

For scenarios requiring guaranteed consistency, RomM provides the `confirm_download` endpoint (POST `/saves/{id}/downloaded`). This explicit confirmation flow forces a sync entry regardless of optimistic settings:

```python

# Within confirm_download in backend/endpoints/saves.py

db_device_save_sync_handler.upsert_sync(
    device_id=device.id,
    save_id=save_id,
    synced_at=datetime.now(timezone.utc)
)

```

Clients use this endpoint to guarantee the server knows a download succeeded, particularly useful after resuming from network interruptions or when optimistic sync is disabled.

### Tracking Control

Users can stop tracking a save on specific devices without deleting the save file itself. The `untrack_save` and `track_save` endpoints in [`backend/endpoints/saves.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/saves.py) toggle the `is_untracked` flag via the handler's `set_untracked` method:

```python

# Untrack disables tracking for this device/save combination

db_device_save_sync_handler.set_untracked(device_id, save_id, untrack=True)

# Track re-enables it

db_device_save_sync_handler.set_untracked(device_id, save_id, untrack=False)

```

When `is_untracked=True`, the device appears in sync lists but is marked as intentionally not tracking the save, preventing false "out of sync" warnings.

## Reading Sync State in API Responses

Every time the API returns save data, it includes the complete sync history so clients can render synchronization status. The `_build_save_schema` function calls `_syncs_for_save`, which invokes `DBDeviceSaveSyncHandler.get_syncs_for_saves` to fetch all device associations:

```python

# Within backend/endpoints/saves.py

def _syncs_for_save(save_id: int) -> list[tuple[DeviceSaveSync, str]]:
    return db_device_save_sync_handler.get_syncs_for_saves([save_id])

```

The resulting list of `(DeviceSaveSync, device_name)` tuples transforms into `DeviceSyncSchema` entries attached to the `SaveSchema` payload. Clients receive data structured like this:

```typescript
// Response from /api/saves
{
  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,
    },
    {
      device_id: "device-xyz",
      device_name: "Desktop",
      last_synced_at: "2024-07-01T09:00:00Z",
      is_untracked: true,
      is_current: false,
    }
  ]
}

```

This structure allows front-end interfaces to display "last synced at" timestamps, highlight which device holds the current version, and indicate untracked devices that should be ignored for update notifications.

## Client-Side Implementation

Frontend applications interact with the synchronization system through standard REST API calls. To fetch saves with their sync status for a specific device:

```typescript
// Frontend implementation using generated types
const response = await api.get<SavesResponse>('/api/saves', {
  params: { device_id: currentDevice.id },
});

// Check if current device has the latest version
const save = response.data.saves[0];
const mySync = save.device_syncs.find(s => s.device_id === currentDevice.id);
const isCurrent = mySync?.is_current ?? false;

```

The frontend can also explicitly confirm downloads or toggle tracking:

```typescript
// Confirm a download completed successfully
await api.post(`/api/saves/${saveId}/downloaded`);

// Stop tracking this save on current device
await api.post(`/api/saves/${saveId}/untrack`);

```

## Summary

RomM's save state synchronization system provides robust cross-device tracking through:

- **A junction table architecture** using `DeviceSaveSync` in [`backend/models/device_save_sync.py`](https://github.com/rommapp/romm/blob/main/backend/models/device_save_sync.py) to link devices and saves with timestamps and tracking flags.
- **Atomic upsert operations** via `DBDeviceSaveSyncHandler.upsert_sync` in [`backend/handler/database/device_save_sync_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/database/device_save_sync_handler.py) that handle creation and updates in a single database transaction.
- **Lifecycle-aware endpoints** in [`backend/endpoints/saves.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/saves.py) that automatically record sync states during uploads, downloads, and explicit confirmations.
- **Flexible tracking controls** allowing users to mark devices as untracked for specific saves without deleting data.
- **Comprehensive API responses** that include per-device sync metadata, enabling clients to display accurate synchronization status and resolve conflicts locally.

## Frequently Asked Questions

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

RomM determines the latest save state by comparing the `last_synced_at` timestamps across all `DeviceSaveSync` entries for a given save. The endpoint logic in [`backend/endpoints/saves.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/saves.py) calculates the `is_current` flag during the `_build_save_schema` process, marking the device with the most recent timestamp as holding the authoritative version. Clients receive this flag in the API response and can immediately identify which device possesses the freshest data without comparing timestamps locally.

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

When you mark a save as untracked via the `/api/saves/{id}/untrack` endpoint, RomM sets the `is_untracked` flag to `True` in the `DeviceSaveSync` table for that device-save pair. According to the implementation in [`backend/handler/database/device_save_sync_handler.py`](https://github.com/rommapp/romm/blob/main/backend/handler/database/device_save_sync_handler.py), this prevents the sync system from considering that device when determining which holds the current version, while preserving the historical sync record. The device remains in the response list but is visually distinguished in the UI, indicating that the user intentionally chose not to synchronize this save to that particular device.

### Can RomM handle save conflicts between multiple devices?

RomM handles conflicts by exposing the complete sync history to clients through the `device_syncs` array in the `SaveSchema` response. Each entry includes the `last_synced_at` timestamp and `is_current` status. While the server records all synchronization events via `DBDeviceSaveSyncHandler`, it does not automatically merge conflicting save files. Instead, clients use this metadata to prompt users with conflict resolution options, showing exactly when each device last synced and which version is considered current based on the most recent timestamp in the database.

### How do I manually confirm that a device successfully downloaded a save?

Use the explicit confirmation endpoint `POST /api/saves/{id}/downloaded` to manually record a successful download. According to the `confirm_download` function in [`backend/endpoints/saves.py`](https://github.com/rommapp/romm/blob/main/backend/endpoints/saves.py), this endpoint forces an immediate call to `db_device_save_sync_handler.upsert_sync`, creating or updating the sync record with the current UTC timestamp. This is particularly useful when optimistic synchronization is disabled or when confirming downloads after network interruptions, ensuring the server accurately reflects that the device now possesses the latest save state.