RomM Save State Synchronization: How It Tracks Saves Across Devices

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. This junction table links devices to their saves using a composite primary key:


# 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 provides the data-access layer for managing sync records. Its primary method, upsert_sync, creates or updates sync entries atomically:


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


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


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


# 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 toggle the is_untracked flag via the handler's set_untracked method:


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


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

// 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:

// 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:

// 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 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 that handle creation and updates in a single database transaction.
  • Lifecycle-aware endpoints in 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 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, 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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →