> For the complete documentation index, see [llms.txt](https://docs.kouch.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kouch.dev/docs/adr/0007-user-data-folders-and-cloud.md).

# ADR 0007: Relocatable emulator user data, Steam Cloud, and LAN transfer/play

## Status

Accepted (2026-09-18, owner decisions — see the sequence in "Context").

## Context

The owner asked whether Kouch could link a user's Dropbox/Google Drive folder so emulator saves — and, they floated, BIOS/firmware/keys — could follow them between machines. That second half is a straightforward no under ground rule 2 ("Never ship, download, transfer, or bundle ROMs, BIOS, firmware, or keys") and the 2023 precedent of a well-known emulator being pulled from a storefront over a decryption key shipped in its binary — Kouch must never be the thing that moves that file, on any transport, cloud or LAN. The saves half is a real, common need (an emulator's own save/state/config files are just data Kouch already manages a folder for) with no such problem.

The decision on *how* to move that data changed shape twice in one session as the owner refined the ask:

1. First: only a relocatable, Kouch-managed data folder (no cloud of any kind) — the user drops it in their own Dropbox/Drive folder if they want that.
2. Then: also build Steam Cloud sync, since Kouch already runs inside Steam and every player already has an account.
3. Then: also build local-network peer-to-peer transfer between two machines running Kouch, for players without (or who don't want) Steam Cloud, plus — a related but distinct feature — local co-op play by forwarding a second PC's controllers to the host over the same LAN link ("Mode A", controller forwarding — works with any emulator, no netplay support required).

All three ended up in scope together, not as alternatives.

## Decision

* **`EmulatorProfile.user_data`** (`kouch_profiles::userdata::UserData`): `folders` (a `BTreeMap<String, String>`, defaulting to `saves`, `states`, `screenshots`, `config`, `system` each named after its own key so an author can omit the block entirely), `cloud` (which folders may sync to Steam Cloud — never `system`), `config_edits` (templated edits to the emulator's own config file, `crates/kouch-profiles/src/config_edit.rs`, backing up the original once before the first edit). `Settings.user_data_root` (default `<data>/userdata`) is the relocatable root; `kouch-launch` creates every folder and applies `config_edits` before every spawn.
* **The `system` folder is the one place ground rule 2 draws a hard, mechanically-enforced line.** It is where the *user* copies their own BIOS/firmware/keys. Kouch creates the folder, can open it in the OS file browser (`user_data_open_folder`), and learns exactly one bit about it — `system_folder_present` (exists and non-empty) — via a bare `read_dir().next().is_some()`. It is never listed in a manifest, never read, never synced to Steam Cloud (`user_data.cloud` cannot name it — validated), and never sent or received over LAN transfer — enforced independently on both the sending side (`kouch_lan::manifest::is_transferable_folder`) and the receiving side (`kouch_lan::transfer::receive_sections`, which refuses a `system`-keyed section or a smuggled `system/`-rooted path even from a sender that claims otherwise).
* **Steam Cloud** (`app/src-tauri/src/cloud.rs`): per-profile opt-in (`EmulatorProfile::cloud_sync`), uploads on game exit and on demand (`cloud_sync_now`), downloads anything newer before every launch and once at Kouch startup. Every `ISteamRemoteStorage` call runs off the input thread on a cloned, `Send + Sync` `steamworks::Client` (`kouch_input::InputSource::steam_client`) — not the `!Send` `SteamCtx` ground rule 4 keeps on the input thread, and not `RemoteStorage` itself, which holds raw pointers. Skipped entirely under the borrowed dev app id (`crate::input::is_borrowed_app_id`) so a pre-launch build never writes into another game's cloud slot on the owner's account.
* **LAN transfer and LAN play** (`crates/kouch-lan`, `app/src-tauri/src/lan.rs`): one small crate holds the whole protocol — mDNS discovery (`_kouch._tcp.local.`, LAN-address-only), SPAKE2 pairing from a 6-digit code with HKDF-SHA256 key derivation, a ChaCha20-Poly1305-encrypted TCP control channel (length-prefixed frames, disjoint per-direction nonce spaces), and an encrypted UDP channel for LAN play's per-tick pad forwarding (sequence-numbered, drop-old). Nothing runs unless `settings.lan.enabled`. LAN play reuses the exact same claim/seat/DSU pipeline a local pad goes through: a forwarded seat is keyed as an ordinary `SourceId::Os` in a reserved id range so `kouch-input`'s identity-merge logic needs no changes, with only `ControllerKind::Remote` (a new, brand-neutral, fieldless variant) telling the rest of Kouch it arrived over the network.

## Addendum (2026-09-19): one container format, `.kod`

The community-profile file kinds this ADR and `docs/DESIGN.md` §10c.1 originally shipped with — `.koe` (one profile), `.koes` (a JSON bundle, `{"version":1,"profiles":[...]}`), `.kom`/`.koms` (mods, reserved) — lasted one day. The owner replaced all four with a single `.kod`: a zip (`app/src-tauri/src/kod.rs`) holding `kod.json` (`{kod: 1, name, contents: {profiles, data, mods}}`), `profiles/<id>.json` per profile, and an optional `data/<id>/<key>/...` per profile for exactly the same user-data folders this ADR already covers (**never `"system"`, enforced independently on write and on read** — `write_kod` skips it outright and `KodArchive::extract_data`/`data_summary` refuse it again regardless of what a hand-edited `kod.json` or a crafted zip entry claims, reusing `kouch_lan::manifest`'s exact `is_transferable_folder`/ `is_safe_relative_path`/`starts_with_system` semantics — tested against a hand-built hostile zip). `kind` (`emulator`/`emulator_bundle`/`backup`/ `mod`/`mod_pack`) is derived from what the archive actually contains, not stored, so a hand-edited manifest can't lie about it. New commands `kod_import` (read + save/install chosen profiles + restore chosen data folders, one call) and `kod_export` (write, refusing a destination inside `<data>/emulators` or the user-data root); `profiles_import_bundle`'s signature changed to take already-parsed profiles (the confirmation Sheet's own `bundle` from `profile_inspect`) rather than re-fetching a now-nonexistent `.koes` file. `kouch_profiles::ProfileBundle` (the `.koes` JSON shape) is removed as dead code.

**Refinement (2026-09-19): `.kod` is the only user-facing extension.** A hand-written profile is simply that `EmulatorProfile` JSON saved with a `.kod` extension — no zip, no manifest, no separate `.json` format; a `.kod`'s bytes are told apart as a zip archive or plain JSON purely by content (the zip magic bytes, `kod::looks_like_zip`), never by file extension or a URL's declared content type, in both `profile_inspect` and `kod_import` (a zip-less `.kod` is one profile — kind `emulator`, a bundle of one, no data — saved/installed the same as any other). Drag-drop and deep-link accept `.kod` primarily; `.json`, `.kouch-profile`, `.koe` and `.koes` are still accepted as silent legacy aliases (no UI mention). `ProfileImportRequest.kind` is now always `"kod"` (kept `Option<String>` for compat, and ignored by the UI — the file's real shape is `ProfileInspection.kind`, known only once it's actually opened).

## Consequences

* Saves/states/screenshots/config can now follow a player between machines three ways — a folder they sync themselves, Steam Cloud, or a direct LAN transfer — while BIOS/firmware/keys categorically cannot move through Kouch by any of them; ground rule 2 holds regardless of which sync path a profile or a session uses.
* `kouch-input` gained one new dependency-light `InputSource` (`LanSource`) and one new `ControllerKind` variant; every other crate that matches `ControllerKind` exhaustively (`kouch-steam`'s `ConfigMask`) needed one new arm — a small, one-time ripple, not a recurring cost.
* The Steam Cloud write path and the whole LAN protocol's live behavior (mDNS across a real second machine, an actual paired transfer, a real co-op session) are unverified in this environment — `crates/kouch-lan`'s own test suite covers the protocol logic itself end-to-end over an in-memory duplex pipe (pairing, mismatched-code failure, the full offer/accept/transfer round trip, the `system`-exclusion hard rule from both a malicious `FolderStart` and a smuggled file path, the LAN-play join/claim control exchange, and the UDP datagram codec including replay/reorder handling) but the real sockets, the real Steam Cloud calls, and the real second-machine scenarios need the owner's hardware.
* Mode B (emulator-native netplay via `config_edits` + a game-hash/version check, `docs/FEATURE_PLAN.md` §6 "per-emulator netplay adapters") is a deliberate non-goal of this ADR — LAN play here is controller forwarding only, which needs no per-emulator support at all.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kouch.dev/docs/adr/0007-user-data-folders-and-cloud.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
