Skip to content
Joël Deffner edited this page Sep 4, 2026 · 2 revisions

Cloud

Since v0.3.0. steam.cloud is the curated layer over ISteamRemoteStorage: read, write, list and delete Steam Cloud files for the current user and app.

Reads and writes go through Valve's async path (FileWriteAsync, FileReadAsync), so a slow disk or a large file never blocks the Node event loop. Those two are async, and so are share and writeBatch; everything else here is a local read or a one-line synchronous call. A non-OK EResult becomes a SteamResultError, and a flat call that returns false becomes a plain Error. See Errors.

A file only reaches Valve's servers when cloud sync is on for both the account and the app. Check that with isEnabledForAccount and isEnabledForApp; local reads and writes work either way.

The Cloud instance is created lazily and cached on the Steam object. The workshop lives in Workshop, not here: the legacy publish-to-workshop calls that also sit on ISteamRemoteStorage are deprecated and are not wrapped.

writeFile

writeFile(name: string, data: Buffer | string): Promise<void>

Writes one file, replacing it when it already exists.

Parameter Type Meaning
name string File name, for example save01.json. Forward slashes make subfolders.
data Buffer | string The contents. A string is encoded as UTF-8.

Steam rejects an empty write and caps one file at 100 MB. The promise resolving means the file was written locally; Steam syncs it afterwards, so use isPersisted to find out whether the cloud copy is current.

await steam.cloud.writeFile('save01.json', JSON.stringify({ level: 3 }));

Throws SteamResultError with operation: 'FileWriteAsync' on a non-OK result, for example k_EResultLimitExceeded when the quota is full.

writeBatch

Since v0.6.0.

writeBatch<T>(fn: () => Promise<T> | T): Promise<T>

Groups several writes into one save, so Steam syncs them together. A save game spread over more than one file is only consistent as a set: inside the callback Steam holds the sync back, and it starts one sync for everything written once the callback finished.

await steam.cloud.writeBatch(async () => {
  await steam.cloud.writeFile('save01/meta.json', JSON.stringify({ level: 3 }));
  await steam.cloud.writeFile('save01/world.bin', worldBytes);
});

The callback is awaited, so it may be async, and its return value comes back from writeBatch. Anything it does that is not a cloud write is allowed; it just delays the sync. The batch is always closed, even when the callback throws, and the callback's own error is the one that reaches the caller.

Throws Error: steamwand: BeginFileWriteBatch returned false (...) when Steam refuses to open the batch, for example because one is already open.

readFile

readFile(name: string): Promise<Buffer>

Reads one whole file. Three flat calls in a row: GetFileSize for the length, FileReadAsync to queue the read, then FileReadAsyncComplete to copy the bytes Steam holds into a buffer sized to what Steam reports it actually read, not to what GetFileSize said. Steam frees its own copy in that last call, so it runs even for a short read.

An empty file gives an empty buffer without any async call at all.

const save = JSON.parse((await steam.cloud.readFile('save01.json')).toString('utf8'));
console.log(save.level);

Throws:

  • Error: steamwand: cloud file does not exist: <name> before any native call.
  • SteamResultError with operation: 'FileReadAsync', for example k_EResultFileNotFound.
  • Error: steamwand: FileReadAsyncComplete returned false (...) when Steam refuses the read handle.

deleteFile

deleteFile(name: string): void

Deletes the file from the cloud and from disk. The file is gone everywhere and stops counting against the quota. This is the one a "delete save game" button wants. Compare forgetFile, which sounds similar and does something quite different.

Throws Error: steamwand: FileDelete returned false (...), which usually means the file does not exist.

forgetFile

forgetFile(name: string): void

Stops syncing the file without deleting anything. The local copy stays on this machine, the cloud copy stays on Valve's servers, and the two stop tracking each other, so the file is not downloaded onto the user's other machines. Use it for a save the user wants on one machine only.

A forgotten file never becomes persisted again, so isPersisted stays false for it.

Throws Error: steamwand: FileForget returned false (...) when the file does not exist.

exists

exists(name: string): boolean

True when the file is in this app's cloud storage, locally or in the cloud.

isPersisted

isPersisted(name: string): boolean

True when the cloud copy is up to date. Expect false right after a write and true once the sync finished.

await steam.cloud.writeFile('save01.json', data);
console.log(steam.cloud.isPersisted('save01.json')); // usually false, briefly

listFiles

listFiles(): CloudFile[]

Every file in this app's cloud storage for the current user, in Steam's own order. Empty when the app has none.

for (const f of steam.cloud.listFiles()) console.log(f.name, f.sizeBytes);

getFileInfo

getFileInfo(name: string): CloudFileInfo | null

Size and modification time without reading the contents. null when the file does not exist, so this is also a way to ask "how big is it" and "is it there" in one call.

const info = steam.cloud.getFileInfo('save01.json');
if (info) console.log(info.sizeBytes, new Date(Number(info.timestamp) * 1000));

share

Since v0.6.0.

share(name: string): Promise<bigint>

Publishes a cloud file and returns the UGCHandle_t that points at it. The handle is public: anybody who has it can download the file, so only share what the user meant to share.

That handle is what a leaderboard entry carries through attachUgc, which is how a score gets a replay attached to it.

await steam.cloud.writeFile('replay01.bin', replayBytes);
const ugc = await steam.cloud.share('replay01.bin');
const board = await steam.leaderboards.find('Feet Traveled');
if (board) await steam.leaderboards.attachUgc(board.handle, ugc);

Throws SteamResultError with operation: 'FileShare', for example k_EResultFileNotFound.

syncPlatforms

Since v0.6.0.

syncPlatforms(name: string): number

Which platforms the file syncs to, as an ERemoteStoragePlatform bit mask: 1 Windows, 2 macOS, 8 Linux, 16 Switch, 32 Android, 64 iOS, and -1 for all.

setSyncPlatforms

Since v0.6.0.

setSyncPlatforms(name: string, platforms: number): void

Limits a file to certain platforms. Use it for a file that only makes sense on one, for example key bindings a controller-only build cannot read. Steam keeps the file either way, it just stops downloading it elsewhere. Or the bits together for several platforms.

const desktop =
  flat.ERemoteStoragePlatform.k_ERemoteStoragePlatformWindows |
  flat.ERemoteStoragePlatform.k_ERemoteStoragePlatformLinux;
steam.cloud.setSyncPlatforms('bindings.cfg', desktop);

Throws Error: steamwand: SetSyncPlatforms returned false (...), which usually means the file does not exist.

quota

quota(): CloudQuota

How much cloud storage this app may use for this user, and how much is left. Both counts are bigint. A write larger than availableBytes fails with k_EResultLimitExceeded, so check this before writing something big.

const { totalBytes, availableBytes } = steam.cloud.quota();
console.log(`${availableBytes} of ${totalBytes} bytes free`);

Throws Error: steamwand: GetQuota returned false (...) when the quota is not known yet.

isEnabledForAccount

isEnabledForAccount(): boolean

Whether the user turned cloud sync on for their whole account. They set this in the Steam client settings and the app cannot change it. Files still write locally when it is off, they just never sync.

isEnabledForApp

isEnabledForApp(): boolean

Whether cloud sync is on for this app. Steam takes the starting value from the user's per-app setting.

setEnabledForApp

setEnabledForApp(enabled: boolean): void

Turns cloud sync on or off for this app, for this session. Meant for an in-game "sync my saves" option. It does not change the user's Steam client setting, and it cannot switch sync on while the account has it off. Steam gives no result here, so it cannot fail from JavaScript.

onLocalFileChange

Since v0.6.0.

onLocalFileChange(listener: () => void): () => void

Subscribes to the "Steam just changed files on disk" notice and returns an unsubscribe function. Steam fires it once after a sync changed files while the app was running, for example because the user played on another machine and came back. The callback carries nothing; listLocalChanges says which files.

const off = steam.cloud.onLocalFileChange(() => {
  for (const c of steam.cloud.listLocalChanges()) console.log(c.name, c.change);
});
// later: off();

listLocalChanges

Since v0.6.0.

listLocalChanges(): { name: string; change: 'updated' | 'deleted'; pathType: 'absolute' | 'apiFilename' }[]

The files a Steam sync changed underneath the running app. Steam clears the list on the next sync, so read it from an onLocalFileChange listener rather than polling for it.

A game that keeps a save open in memory uses this to notice that the copy on disk moved on, and to offer the player a reload instead of overwriting it.

Field Type Meaning
name string The file, named the way pathType says.
change 'updated' | 'deleted' From ERemoteStorageLocalFileChange.
pathType 'absolute' | 'apiFilename' From ERemoteStorageFilePathType. apiFilename names it the way the rest of this layer does; absolute is a full path on disk.

Empty when nothing changed.

Types

CloudFile

One entry from listFiles.

Field Type Meaning
name string File name, the same name readFile and deleteFile take.
sizeBytes number Size in bytes. Steam caps one file at 100 MB, so a number is enough.

CloudFileInfo

Returned by getFileInfo.

Field Type Meaning
sizeBytes number Size in bytes.
timestamp bigint Last write time, Unix seconds. 64-bit, so a bigint.

CloudQuota

Returned by quota.

Field Type Meaning
totalBytes bigint Total bytes Steam grants this app for this user.
availableBytes bigint Bytes still free.

Errors

Shape When
SteamResultError FileWriteAsync, FileReadAsync or FileShare completed with a non-OK EResult. Common values: k_EResultLimitExceeded (quota full), k_EResultFileNotFound, k_EResultDiskFull.
Error: steamwand: cloud file does not exist: <name> readFile, before it calls anything native.
Error: steamwand: <call> returned false (invalid handle or argument?) FileDelete, FileForget, FileReadAsyncComplete, SetSyncPlatforms, BeginFileWriteBatch or GetQuota returned false.
SteamApiCallError The read or the write never produced a usable result, or completed carrying a different callback struct. See How It Works.

What this layer does not do

The rest of ISteamRemoteStorage is on the raw generated steam.remoteStorage:

  • The synchronous FileWrite and FileRead, which block on disk. The async pair is wrapped instead, on purpose.
  • Streaming writes: FileWriteStreamOpen, ...WriteChunk, ...Close, ...Cancel, for a file assembled in pieces.
  • Downloading somebody else's shared file: UGCDownload, UGCRead, GetUGCDetails and the cached-UGC calls, which all take the handle share hands out.
  • The whole deprecated PublishWorkshopFile family. Use Workshop instead.

Those calls take raw Buffer out params and return flat booleans or call handles. Flat API explains the calling convention.

Clone this wiki locally