Open-source helper apps for self-hosted retro save sync.
This repo is built for homelab emulator users. You run a helper on each device (MiSTer, Steam Deck, Windows, and more), and each helper syncs local saves with your self-hosted backend.
- A running Save Game Manager backend reachable on your LAN.
- A device helper binary from Releases.
- A
config.inifile next to the helper binary. - One auth method:
- App password (
login --email --app-password), or - Backend "Add helper" enroll flow.
- App password (
If backend auth is disabled, you can usually run sync directly.
For always-on devices, use service install after the first successful sync. Service mode keeps the helper connected, sends health sensors to the backend, and reacts to backend "Sync now" events.
- MiSTer:
sgm-mister-helper-armv7.tar.gz - Anbernic/KNULLI/Batocera ARM64:
sgm-anbernic-helper-aarch64-unknown-linux-musl.tar.gz - Steam Deck:
sgm-steamdeck-helper-x86_64-unknown-linux-gnu.tar.gz - Windows:
sgm-windows-helper-x86_64-pc-windows-gnu.zip - GameCube/Wii:
sgm-gamecube-helper.dol - Nintendo 3DS:
sgm-3ds-helper.3dsx - Checksums:
sha256.txt
- Copy
sgm-mister-helperto your MiSTer (for example/media/fat/). - Create
/media/fat/config.ini:
URL="192.168.2.10"
PORT="80"- First sync:
cd /media/fat
./sgm-mister-helper sync- If auth is enabled, do one of these first:
./sgm-mister-helper login --email you@example.com --app-password YOUR_APP_PASSWORD
./sgm-mister-helper syncOr:
- Open backend UI.
- Click
Add helper. - Run
./sgm-mister-helper syncwithin 15 minutes.
URL: backend IP or hostname, nohttp://.PORT: backend port.
That is enough for first run. The helper will auto-scan known save locations on first sync when no sources are configured.
- Copy
sgm-steamdeck-helperto a folder, for example/home/deck/SGM-Helper/. - Create
/home/deck/SGM-Helper/config.ini:
URL="192.168.2.10"
PORT="80"- First sync:
cd /home/deck/SGM-Helper
./sgm-steamdeck-helper sync- If needed, login once:
./sgm-steamdeck-helper login --email you@example.com --app-password YOUR_APP_PASSWORD
./sgm-steamdeck-helper syncURL: backend IP or hostname, nohttp://.PORT: backend port.
On first run, the helper tries known paths (including common EmuDeck-style save locations), then stores sources in config.ini.
When you run sync or watch for the first time and no [source.*] sections exist:
- The helper runs a known-path scan.
- Found sources are written into
config.iniasMANAGED="true". - Next runs use those stored sources.
Use these scan controls:
--scan: rescan known emulator paths and refresh onlyMANAGED="true"sources.--deep-scan: broad scan, write candidates toSTATE_DIR/scan_report.json(review only).--deep-scan --apply-scan: write deep-scan candidates intoconfig.ini.
Default path: same folder as the binary (./config.ini).
Precedence: CLI flags > ENV > config.ini > defaults.
URL="192.168.2.10"
PORT="80"
EMAIL=""
APP_PASSWORD=""
ROOT="/media/fat"
STATE_DIR="./state"
WATCH="false"
WATCH_INTERVAL="30"
FORCE_UPLOAD="false"
DRY_RUN="false"
ROUTE_PREFIX=""URL: backend host/IP without scheme.PORT: backend port.EMAIL: optional default email for auth commands.APP_PASSWORD: optional default app password.ROOT: optional scan root.STATE_DIR: helper state folder (auth.json, sync state, lockfile).WATCH: default watch mode.WATCH_INTERVAL: polling interval in seconds for watch mode.FORCE_UPLOAD: force upload preference.DRY_RUN: dry-run preference.ROUTE_PREFIX: optional API prefix, for examplev1.
- MiSTer:
/media/fat - Steam Deck:
/home/deck/.steam/steam/steamapps/compatdata - Windows:
./saves
The helper stores save source mappings in config.ini:
[source.super_nintendo]
LABEL="Super Nintendo"
KIND="retroarch"
PROFILE="snes9x"
SAVE_PATH="/home/deck/Emulation/saves/snes"
ROM_PATH="/home/deck/Emulation/roms/snes"
RECURSIVE="true"
SYSTEMS="snes"
CREATE_MISSING_SYSTEM_DIRS="false"
MANAGED="false"
ORIGIN="manual"Each key:
LABEL: display name.KIND: runtime/source kind (mister-fpga,retroarch,custom, ...).PROFILE: emulator profile mapping (for extension behavior).SAVE_PATH: save folder path.ROM_PATH: ROM folder path (optional, recommended).RECURSIVE: include subfolders.SYSTEMS: comma-separated console allow-list for this source, for examplesnes,n64,psx.CREATE_MISSING_SYSTEM_DIRS: iffalse, cloud restore only writes into existing system folders.MANAGED:trueif helper manages this source during scans.ORIGIN: metadata (manual,scan,deep-scan,first-run).
Helpers do not blindly download every save from the backend. Each source has a SYSTEMS allow-list.
- MiSTer defaults to FPGA-supported systems only:
nes,snes,gameboy,gba,n64,genesis,master-system,game-gear,sega-cd,sega-32x,saturn,neogeo,psx. - Steam Deck and Windows default to the broad helper list, including Wii and Sony systems where local emulator folders exist.
CREATE_MISSING_SYSTEM_DIRS="false"prevents accidental folder creation, for example a MiSTer helper will not create/media/fat/saves/Wii.- To opt in manually, add the console slug to
SYSTEMSand create the target system folder yourself, or setCREATE_MISSING_SYSTEM_DIRS="true".
During sync, watch, and service run, helpers send a parsed config snapshot to the backend at POST /helpers/config/sync. If the backend returns policy, that policy is written back to config.ini and applied to the current run.
Before backend writeback, the helper creates a timestamped backup next to the config file, for example config.ini.backend.20260425123000.
Backend-created sources are stored as normal [source.<id>] sections. This means the backend UI can add a console/profile/path before any save exists locally, for example Super Nintendo + Snes9x + /media/snes9x/saves.
See backend.md for the full backend contract.
The MiSTer, Steam Deck, and Windows helpers share the same command set.
signuploginresend-verificationlogouttokensyncwatchconvertsource listsource add custom|mister-fpga|retroarch|openemu|analogue-pocketsource remove --name <id>state liststate clean --missing|--allconfig showschedule install|status|uninstallservice run|install|status|uninstalldevice-auth
Use these before any command:
--config <path>--url <host>--api-url <host:port or url>--port <port>--email <email>--app-password <password>--root <path>--state-dir <path>--route-prefix <prefix>--verbose--quiet
--force-upload[=true|false]--dry-run[=true|false]--scan--deep-scan--apply-scan(used with--deep-scan)--slot-name <name>(PlayStation slot hint)watchonly:--watch-interval <seconds>
--heartbeat-interval <seconds>: how often the helper reports online status to the backend. Default:30.--reconcile-interval <seconds>: periodic full sync even when no backend event arrives. Default:1800.--force-upload[=true|false]--dry-run[=true|false]--scan--deep-scan--apply-scan--slot-name <name>
Use service mode when possible. It is better than a simple timer because the helper stays online, reports health sensors, and can react when the backend sends a sync event.
cd /media/fat
./sgm-mister-helper service install
./sgm-mister-helper service statuscd /home/deck/SGM-Helper
./sgm-steamdeck-helper service install
./sgm-steamdeck-helper service statusRemove service:
./sgm-mister-helper service uninstall
./sgm-steamdeck-helper service uninstallNotes:
- Linux helpers use systemd when available and fall back to a marked
@rebootcron entry. - Windows helper uses Task Scheduler on logon.
- Service mode runs
service run --quiet. - It sends heartbeat sensors to
POST /helpers/heartbeat. - It listens to backend events on
GET /events. - It still reconciles every 30 minutes by default.
- Overlapping syncs are prevented via
STATE_DIR/sync.lock.
If your device cannot keep a service running, use the older scheduler mode:
./sgm-mister-helper schedule install --every-minutes 30
./sgm-mister-helper schedule statusRemove schedule:
./sgm-mister-helper schedule uninstall- MiSTer:
docs/mister/install.md - Steam Deck:
docs/steamdeck/install.md - Windows:
docs/windows/install.md - GameCube:
docs/gamecube/install.md - 3DS:
docs/3ds/install.md - Backend service contract:
service.md