Self-hosted save synchronization and version control for games across multiple devices.
Quick Start · Usage · RomM · Your Data · Security · Development
If you play games across multiple devices, you know the problem: your handheld has an older save than your home console, and you have to remember which one is current — or manually transfer files before every session.
OmniSave solves this. When a save changes on one device, OmniSave automatically archives the previous version, makes the new save the active version, and distributes it to your other devices. Every previous version is kept. Nothing is ever silently overwritten.
- Saves stay in sync automatically. When a save changes on any device, the others are updated without you doing anything.
- Every version is kept. OmniSave never throws away a save. Every previous version is archived and can be pushed back to any device from the web dashboard.
- Interrupted syncs recover on their own. If a transfer is cut off — network drop, crash, whatever — it resumes where it left off.
- Multiple users on one server. Each person has their own devices, saves, and sync state. Devices can be shared with other users.
- Multiple player profiles on one device. Each in-game profile maps to an OmniSave account, so saves go to the right person.
- Turn off sync per game, per device. If you don't want a particular game syncing to a particular device, you can disable it from the dashboard without affecting anything else.
- Full web dashboard. See the sync state of every game across every device, browse version history, push any save to any device, and manage errors — all from a browser.
- Optional RomM integration. If you use RomM, connecting it gives you cover art in the dashboard and bridges saves between physical devices and emulators automatically. Not required for anything else.
- Docker and Docker Compose
# compose.yml
services:
omnisave:
image: ghcr.io/kanjieater/omnisave:latest
restart: unless-stopped
ports:
- "8991:8991"
volumes:
- ./data:/app/data
- ./config:/app/configdocker compose up -dOpen http://<your-host>:8991 in a browser.
First-run checklist:
- Log in with
admin/admin. - Change the admin password immediately (Settings → Account).
- Install a client on your device — see Clients below.
- Pair your first device (Devices → Add Device).
- Optionally create additional user accounts (Admin → Users).
When a device with the OmniSave client installed connects to the server for the first time, it displays a short pairing code in its overlay. To pair it:
- Open the web dashboard → Devices → Add Device.
- Enter the pairing code shown on the device.
- The device receives its token automatically on its next poll — no manual configuration needed on the device side.
- Optionally rename the device and set a default player profile.
Normal operation requires no interaction. When a save changes on a device, it uploads to the server, and the server queues it for delivery to every other device that has that game installed. The other devices pick it up on their next poll and restore it before the next game launch.
If you want to send a particular snapshot to a device — say, an older version, or a save from a different device — go to the game in the dashboard, find the snapshot in the history, and hit Push. You can choose which device(s) to send it to and which player profile it should land on.
Dashboard → Devices → (device) → Restore All queues the latest available save for every installed game on that device. Useful after a device reset or reinstall.
Dashboard → Games → (game):
- See every saved version, when it was made, and which device it came from.
- Delete individual snapshots you no longer want.
- Push any version back to any device.
Dashboard → Playtime:
- A heatmap shows every day you played, with color intensity based on total time.
- Hover or tap any cell to see a per-game breakdown for that day.
- Navigate between years with the chevrons. Year totals and all-time stats update automatically.
- Each OmniSave user sees only their own sessions. On a shared device, sessions are attributed to the right account based on the active Switch profile.
Dashboard → Devices → (device) → Games → toggle the game off. Any pending delivery for that game to that device is cancelled immediately. Other devices are unaffected.
| Variable | Default | Description |
|---|---|---|
OMNISAVE_DATA |
/app/data |
Where the database and save archives are stored. Mount a persistent volume here. |
OMNISAVE_PORT_INTERNAL |
8991 |
Port the server listens on inside the container. |
Create an empty file at /app/config/reset_admin.flag inside the running container. On the next request, the admin password resets to admin and the flag is removed.
docker exec omnisave touch /app/config/reset_admin.flag
# Log in with admin/admin and change the password immediately.RomM integration is entirely optional. OmniSave's core save sync — between physical devices — works independently of RomM. If you only play on physical hardware, you don't need it.
The integration is useful if you also play through an emulator or any client that uses RomM as its save backend (web player, mobile emulator client, etc.). In that case, RomM acts as a bridge: saves from physical devices flow into OmniSave and then out to RomM, so they're immediately available in your emulator without any manual export. Saves made in an emulator flow the other direction — into OmniSave and then out to your physical devices.
Beyond save sync, connecting RomM unlocks cover art and game names throughout the dashboard. Without RomM, games show up as bare title IDs. With it, box art and proper titles are fetched from your library and cached locally, which makes the save history and sync status pages much easier to navigate.
- Cover art and game names in the dashboard, fetched from your RomM library and cached locally.
- Saves from emulators delivered to physical devices — saves made in any RomM-backed emulator are pulled into OmniSave and queued for delivery to your hardware.
- Saves from physical devices delivered to emulators — when a physical device uploads a new save, OmniSave pushes it to RomM automatically.
- Auto-mapping — when a new title is first seen, OmniSave searches your RomM library by game name and links it automatically if exactly one match is found. Titles can also be mapped manually from the game detail page.
- RomM as a virtual device — your RomM instance appears in the device list like any other client, with its own per-game sync state.
- In the dashboard, go to Settings → RomM and enter your RomM URL and API key.
- Ensure the API key has permission to read and write saves in RomM.
- Existing titles will be auto-matched on next upload. You can also map titles manually from the game detail page.
Each user configures their own RomM connection independently from the dashboard. Credentials are stored per-user.
OmniSave keeps every save that successfully made it to the server. Once a save is committed, it stays until you explicitly delete it. This includes saves that have been replaced by newer versions — they're archived, not removed.
The only things the server cleans up automatically are failed or aborted transfers that never produced a complete save, and those are only removed after 7 days.
| Situation | What happens |
|---|---|
| Upload interrupted mid-transfer | Resumes from the last confirmed point. Abandoned sessions expire after 12 hours. |
| Server crashes while saving | Recovered automatically on restart — the save either completes or is cleanly failed. |
| Delivery to a device fails | The save stays queued. Retry from the dashboard or wait for the device to re-try automatically. |
| Two devices save different progress for the same game while offline | Both saves are stored. Neither is discarded. The dashboard shows the state and you can choose which version to push. |
| A save file is identical to what's already on the server | Detected automatically. No extra storage used, no duplicate entry created. |
- Disk durability. The database and save files are on your filesystem. Back up the
data/volume — OmniSave doesn't do this for you. - TLS. Run a reverse proxy in front of OmniSave if you're exposing it outside your home network.
OmniSave is designed for trusted network use — your home LAN, a VPN, or a private VPS. It is not hardened for exposure to the public internet without a reverse proxy.
- Dashboard login uses password hashing and secure session tokens. Sessions are scoped per-login and can be invalidated individually.
- Devices never receive credentials automatically. A device only gets a token after a logged-in user explicitly approves it via the pairing code flow. Removed devices have their tokens revoked immediately.
- Save data never leaves your server unless you have RomM integration enabled, in which case saves are pushed to your own RomM instance.
- Use a reverse proxy with TLS (nginx, Caddy, Traefik).
- Change the admin password before the server is reachable.
- Consider IP-restricting the dashboard if it's on a public host.
OmniSave uses an open REST API. Any client that implements the sync protocol can participate.
Current clients:
- OmniSaveSwitch — a background sysmodule for custom firmware that watches for save changes and handles upload and download automatically.
- REST API — full schema at
/docson your running server, or browse the published API reference.
Future:
- PC client with Playnite integration
- Emulator support via the PC client
- Experimental cross-device save conversion
- Per-game and global data retention policies
The server runs as a single Docker container serving both the REST API and the React dashboard. All state lives in a SQLite database and a flat archive directory — no external dependencies required.
Every save moves through this state machine. Inbound transactions (uploads from devices) are processed and then forked into one outbound transaction per target device.
| Path | Contents |
|---|---|
$OMNISAVE_DATA/omnisave.db |
All metadata: devices, transactions, snapshot history, user accounts |
$OMNISAVE_DATA/archive/ |
Committed save files, one subdirectory per snapshot |
$OMNISAVE_DATA/staging/ |
In-progress uploads; cleaned automatically after processing |
- Backend: Python 3.11, FastAPI, SQLite, uvicorn
- Frontend: React (Vite), served as a static SPA from the same container
- Optional integration: RomM
- Python 3.11+
- Node.js 20+
cd server
pip install -r requirements.txt
pytestThe test suite uses real SQLite databases — no mocking of the database layer.
docker build -t omnisave:latest ./servercd server
pip install -r requirements.txt
cd ui && npm ci && npx vite build && cd ..
OMNISAVE_DATA=/tmp/omnisave python src/main.pyOmniSave/
├── assets/ # Logo and screenshot assets
├── context/ # Design docs and planning artifacts
├── deploy/ # Production compose configs
└── server/
├── Dockerfile
├── src/ # Python server source
├── ui/ # React dashboard source
└── tests/ # pytest test suite
- Fork the repository and create a branch from
main. - Write tests for any behavioral change.
- Ensure
pytestpasses with no regressions. - Open a pull request with a clear description of what changed and why.
Core invariants:
- The server never modifies save content. It stores and retrieves opaque files.
- A committed snapshot is never automatically deleted or overwritten. Only explicit user action through the UI can remove one.
Concurrent play on two devices — last close wins
If the same game is open on two devices at the same time, the last device to close the game becomes the new active version on the server. When device A uploads a save, device B receives it. If device B later closes the game, it uploads its own version (which may reflect older or diverged progress), and that becomes the new active version — overwriting device A's upload.
Workaround: From the game's snapshot history in the dashboard, push the version you want back to the appropriate device.
Saves are stored as-is — no conversion between devices
The server stores and delivers save files as opaque archives. It does not inspect, modify, or convert save content. If two devices use incompatible save formats for the same game (e.g., different regions or hardware configurations), the server will deliver the file without warning.
Identical saves don't create a new version
If a device uploads a save that is byte-for-byte identical to what's already on the server, no new snapshot is created. This is intentional — it avoids duplicate entries for sessions where no actual progress was made — but it means closing a game without playing won't increment the version counter.
Shared devices deliver saves across users
Save delivery is currently scoped to the device, not the user. If two users share the same server and both have a game installed on their respective devices, any upload from either user fans out to all devices in the catalog that have that game installed — including devices belonging to the other user. Similarly, if two users are granted access to the same physical device, both users' saves queue for delivery to it and the last one applied wins.
This affects multi-user households on a shared server. Single-user or per-user server deployments are unaffected.
Dashboard shows "UI not built"
The React UI wasn't compiled into the image. Rebuild the image, or run npm ci && npx vite build in server/ui/ if running locally.
Device shows no pairing code
The device hasn't contacted the server yet. Check that the client is pointed at the correct address and port.
Save delivery stuck as failed
Dashboard → Devices → (device) → Errors → Retry. If the device was reset, use Restore All after re-pairing to re-queue all saves.
RomM game shows out of sync but no delivery is queued
Trigger a manual push from the game's snapshot list, or wait for the next RomM sync cycle.
Admin password lost
docker exec <container> touch /app/config/reset_admin.flag
# Password resets to 'admin' on next request.Storage growing unexpectedly
Failed and incomplete transfers are cleaned up automatically after 7 days. Committed saves are kept until you delete them. To free space, delete snapshots from the game detail page in the dashboard.
Community discussion on KanjiEater's Discord.
If you find my tools useful please consider supporting:
OmniSave uses a split licensing model.
Client software (sysmodule, UI SDK): MIT License — use, modify, and distribute freely.
Server software (this repository): GNU Affero General Public License v3.0 (AGPLv3) — self-host freely. If you modify the server and offer it as a hosted service, the AGPLv3 requires you to publish the modified source.
OmniSave Cloud (official managed hosting): proprietary, operated by the OmniSave project maintainers.





