Cloud Save Architecture for Web Games
The Basic Architecture
A cloud save system has three components: the game client, an authentication layer, and a storage backend. The client serializes game state, attaches the player's authentication token, and sends it to the server via an HTTP request. The server validates the token, identifies the player, and stores the save data in a database keyed to the player's account. On load, the client sends an authenticated GET request, the server retrieves the save, and the client deserializes it.
The authentication layer ensures that players can only read and write their own saves. Without authentication, anyone could overwrite anyone else's data by guessing URLs. OAuth providers (Google, Discord, Apple Sign In) are the standard for web games because they eliminate the need to build password management, email verification, and account recovery. The game receives a JWT (JSON Web Token) or session token after the OAuth flow completes, and includes this token in every API request.
The storage backend can be a traditional database (PostgreSQL, MySQL), a document store (MongoDB, DynamoDB), a real-time database (Firebase Realtime Database, Supabase), or even a simple file store (S3 with player ID as the key path). The choice depends on scale, query requirements, and developer preference. For most indie web games, a managed service with a free tier (Firebase, Supabase, PlanetScale) is the practical starting point.
Backend Options Compared
Firebase is the most popular backend for indie web games. Firebase Authentication provides OAuth flows for Google, Apple, GitHub, and anonymous accounts. The Realtime Database or Firestore stores save data as JSON documents with real-time sync. The JavaScript SDK handles authentication, data persistence, offline caching, and sync with minimal code. Firebase's free Spark plan allows 1 GB of Firestore storage, 10 GB of network transfer per month, and 50,000 daily reads and 20,000 daily writes, which covers most indie games comfortably. The main drawback is vendor lock-in: migrating away from Firebase requires rewriting the authentication and storage layers.
Supabase is an open-source Firebase alternative built on PostgreSQL. It provides authentication (OAuth, email/password, magic links), a REST API auto-generated from the database schema, real-time subscriptions, and row-level security policies. The free tier includes 500 MB of database storage, 5 GB of bandwidth, and 50,000 monthly active users. Because Supabase is built on PostgreSQL, you can use standard SQL for complex queries (leaderboard rankings, analytics, player statistics) that would be awkward in Firebase's document model. The JavaScript client library provides a clean API for auth and CRUD operations.
Custom server with Node.js (Express, Fastify) or Python (FastAPI) gives full control over the API design, database choice, and hosting. A minimal save API has two endpoints: POST /api/saves (write a save) and GET /api/saves (read a save), both requiring an authentication header. The database can be PostgreSQL (for structured data with SQL queries), MongoDB (for flexible document storage), or Redis (for ultra-fast read/write with persistence). Hosting on a VPS (DigitalOcean, Hetzner) costs $5-10 per month and can handle thousands of concurrent players. The tradeoff is maintenance: you manage uptime, security patches, backups, and scaling yourself.
Serverless functions (AWS Lambda, Cloudflare Workers, Vercel Functions) eliminate server management entirely. Each API endpoint is a standalone function that runs on demand and scales automatically. Combined with a managed database (DynamoDB, PlanetScale, Turso), serverless functions provide a cloud save backend that costs nothing at low traffic and scales to millions of requests without infrastructure changes. The cold start latency of serverless functions (50-500 milliseconds for the first request after idle) is acceptable for save operations but noticeable in real-time sync scenarios.
API Design for Game Saves
A well-designed save API is simple and consistent. The core endpoints are:
PUT /api/saves/:slotId stores a save. The request body is the serialized game state (JSON). The server validates the auth token, extracts the player ID, and upserts the save data in the database. The response confirms success and returns the server timestamp. Using PUT (not POST) for saves is semantically correct because saves are idempotent: sending the same save data twice produces the same result.
GET /api/saves/:slotId retrieves a save. The server validates the auth token, queries the database for the specified slot belonging to the authenticated player, and returns the save data as JSON. If the slot does not exist, return a 404 status so the client knows there is no cloud save to load.
GET /api/saves (without a slot ID) returns metadata for all save slots: slot ID, timestamp, level name, play time, and any other display information. The client uses this to populate the save/load screen without downloading the full game state for every slot.
DELETE /api/saves/:slotId deletes a save slot. This is needed for save management UI and for GDPR compliance (right to deletion). Always require authentication and verify that the requesting player owns the slot.
Request validation is critical on the server side. Never trust client data blindly. Validate that the save data is valid JSON, that it does not exceed a reasonable size limit (set a maximum of 1-5 MB per save to prevent abuse), that the slot ID is a valid value (1-3 for a three-slot system), and that the authentication token is valid and not expired. Rate limiting prevents a compromised client from flooding the API with save requests: 1 save per second per player is a reasonable limit.
Conflict Resolution
Conflicts arise when a player plays on two devices without syncing in between. Device A has a save from Tuesday. Device B has a save from Wednesday. The player opens the game on Device A on Thursday. Both saves exist, and the system must decide which one is correct.
Last-write-wins is the simplest strategy. Every save includes a timestamp. When the client loads, it compares the local save timestamp to the cloud save timestamp and uses whichever is newer. This works well when the player uses one device at a time, which is the common case. The failure mode is when the player makes important progress on an offline device, then plays on another device, then the first device syncs. The offline progress is overwritten by the cloud save from the second device.
Player-choice shows both saves to the player and lets them pick. The save/load screen displays the cloud save and the local save side by side with metadata (timestamp, level, play time) and possibly screenshots. The player selects which one to keep. This is the safest option because it never silently discards progress. The cost is UX complexity: the player must make a decision they might not understand ("which of these saves is the one I want?"), and casual players may find it confusing.
Merge-based resolution combines non-conflicting changes from both saves. If the local save has a higher score and the cloud save has more unlocked levels, merge them to get both. This requires game-specific logic to define how each field merges: numeric maximums for scores and levels, union for collections (combine both inventories), and last-modified for everything else. Merge-based resolution is the ideal experience for the player but the most complex to implement. It works best for games with clearly separable state components (progress is separate from inventory, which is separate from settings).
For most indie web games, last-write-wins with a warning is the pragmatic choice. If the local save is newer than the cloud save by more than a few minutes, use the local save and sync it to the cloud. If the cloud save is newer, prompt the player: "A newer save was found on the server. Use the cloud save or keep your local progress?" This catches the most dangerous conflict scenario (accidental overwrite of significant progress) without implementing full merge logic.
Offline-First Architecture
The gold standard for web game saves is offline-first: the game always saves locally first, and cloud sync happens asynchronously in the background. The local save is the source of truth during gameplay. The cloud save is a backup that catches up within seconds when the network is available.
This architecture has several advantages. Save operations are instant (no network latency). The game works perfectly without an internet connection. Network failures, server downtime, and slow connections do not affect the player's experience. The cloud sync layer can retry failed uploads, batch multiple saves into a single request, and handle conflicts without interrupting gameplay.
The sync protocol is: on game start, load the local save and start the game immediately. In the background, fetch the cloud save and compare timestamps. If the cloud save is newer, prompt the player (or auto-apply based on your conflict strategy). During gameplay, save to IndexedDB on every auto-save interval. After each local save, queue a cloud sync. The sync queue processes requests sequentially, retries failures with exponential backoff, and merges responses. On game close, attempt a final sync but do not block the tab from closing.
Firebase and Supabase both provide offline-first capabilities in their client SDKs. Firebase's offline persistence caches data locally and syncs when connectivity resumes. Supabase's realtime subscriptions can detect connection state and queue writes for later delivery. Using these built-in features eliminates the need to build the sync queue from scratch.
Cost Considerations
Cloud save costs scale with two factors: storage volume and request frequency. For a game with 1,000 active players, each with 3 save slots of 100 KB, total storage is 300 MB. At Firebase Firestore pricing ($0.18 per GB per month), storage costs $0.05 per month. Reads and writes cost $0.06 per 100,000 operations. If each player saves 50 times per session and plays 10 sessions per month, that is 500,000 writes per month, costing $0.30. Total monthly cost: under $1 for 1,000 players.
At 100,000 players, costs scale linearly: storage is 30 GB ($5.40/month), and 50 million writes cost $30/month. This is where optimization matters. Compressing save data reduces storage costs. Batching auto-saves (saving locally every 30 seconds but syncing to the cloud every 5 minutes) reduces write costs by 10x. Caching read responses reduces read costs. Delta syncing (sending only changed fields instead of the full save) reduces both bandwidth and write costs.
Free tiers cover most indie games for months or years. Firebase, Supabase, PlanetScale, and Cloudflare Workers all offer generous free tiers. A game needs to reach tens of thousands of daily active players before cloud save costs become a meaningful expense. By that point, the game is likely generating revenue that covers the infrastructure cost many times over.
Cloud saves protect player progress against browser data loss and enable cross-device play. Use Firebase or Supabase for a managed solution with free tiers, implement offline-first architecture so saves are always instant locally, and use last-write-wins with player notification as the conflict resolution strategy for most indie games.