How RomM Synchronizes Save States Across Devices: A Technical Deep Dive
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. 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 tablesave_id: An integer foreign key referencing the specific save filelast_synced_at: A timezone-aware timestamp recording the last synchronizationis_untracked: A boolean flag indicating whether the device should ignore this save
# 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 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:
# 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 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.
// 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:
DeviceSaveSyncmodel inbackend/models/device_save_sync.pystores the junction table linking devices to save files with timestamps and tracking flagsDBDeviceSaveSyncHandlerinbackend/handler/database/device_save_sync_handler.pyprovides atomic operations for creating, updating, and querying sync records viaupsert_syncandset_untracked- Save endpoints in
backend/endpoints/saves.pyorchestrate the sync lifecycle during uploads, downloads, confirmations, and API responses through functions likeconfirm_downloadand_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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →