Writing a cloud provider
The ISaveCloudProvider contract — five operations, honest capabilities, poll-based requests, and what PersistKit does above it so a provider does not have to.
What a provider has to do#
public interface ISaveCloudProvider
{
string Name { get; }
CloudCapabilities Capabilities { get; }
CloudAccountState Account { get; }
string AccountMessage { get; }
CloudConnectRequest Connect();
CloudUploadRequest Upload(CloudUploadSpec spec);
CloudDownloadRequest Download(string remoteKey);
CloudListRequest List();
CloudDeleteRequest Delete(string remoteKey);
}Five operations, and nothing about saves in any of them: bytes out, bytes in. Everything a save system has to get right — deciding which copy wins, refusing to overwrite a copy that moved, remembering what was uploaded, queueing an offline save, retrying — is above the seam and is the same for every provider.
Requests are polled, not blocked on#
PersistKit's runtime does no work off the main thread, so an operation has to make progress while the game runs:
public CloudDownloadRequest Download(string remoteKey)
{
var request = new CloudDownloadRequest(remoteKey);
request.Attach(r => // one step, called whenever the request is read
{
if (r.CancelRequested) { r.CompleteAsCancelled(class="tok-str">"the download"); return false; }
class="tok-com">
// …start your request, then complete when it is done:
r.Complete(PersistResult.Success(true));
return true;
});
return request;
}Attach, Complete, CompleteAsCancelled and ReportProgress are public for exactly this reason. A game uses IsDone, Progress, Result, Cancel() and yield return request.WaitForCompletion().
Four rules that make a provider a good citizen#
- Never throw for an expected failure. A network problem is a
PersistResultwith a code. A thrown exception inside a provider is treated as a defect. - Declare your capabilities honestly.
CloudCapabilities.ConditionalWriteis what makes a lost race a reported conflict instead of a silent overwrite. Claiming it without doing it is how one device destroys another's save. - Report metadata when you can by implementing
ISaveCloudMetadataProvider, so a sync compares two copies without downloading one to find out what it is. - Write in a way that cannot half-replace a save. The folder provider writes a
.partfile and promotes it; the REST provider usesIf-Matchso the endpoint refuses a stale write.
What each service would take#
| Service | What it needs | Where it would plug in |
|---|---|---|
| A folder that syncs (Dropbox, OneDrive, a share) | Nothing | Shipped: FolderCloudProvider |
| Your own HTTP endpoint | A URL, and a place to put a token | Shipped: RestCloudProvider |
| Unity Cloud Save (UGS) | A Unity project ID, the com.unity.services.cloudsave package and a signed-in player | A provider that resolves the player id in Connect() and calls the SDK's save/load — response headers map onto the same codes |
| Steam Cloud | The Steamworks SDK, a Steam App ID, and a signed-in client | A provider over ISteamRemoteStorage: write/read/delete per file, with FileWriteAsync's progress reported through ReportProgress |
| Firebase, Supabase | A project, credentials, and a table or bucket | These are REST APIs, so RestCloudProvider with BaseUrl, an auth header and (for listing) ListUrlTemplate reaches them today. The credentials and the rules are the work |
| PlayFab | A title ID and player authentication | A provider over its file API, with EntityId as the account |
Not verified Those four are described, not implemented. None could be exercised here — each needs an account, a plan or a platform SDK — so no claim is made for any of them, and nothing in the package references their SDKs.
Testing a provider without a network#
MemoryCloudProvider is the double the package's own cloud behaviour is verified against: it can be offline, made slow, made to refuse, and shared between two instances so that "the same save on two devices" is a real test. That verifies PersistKit, not your service: a provider still has to be tested against the real thing.
Next#
Something wrong on this page? Every page here describes behaviour that is checked in the repository. If a page and the package disagree, the package wins.
Report a documentation problem · Error codes · Platforms and limitations