The Retro Save System.
One save system. One code format. One block format. Where you sit on the privacy scale decides what the server sees — and what you hold.
The Sliding Scale
The Retro Save System is one system. Same code format, same block format, same shim. What changes is where on the privacy scale you sit — and that decides what the server sees in exchange for what you hold. Three positions, one slider:
Offline
You hold: the armored block.
Server holds: nothing — no request is made.
Price: cross-device means copy-paste.
Online, Encrypted
You hold: a 16-char code, plus an optional block for backup.
Server holds: ciphertext only.
Price: lose the code or the block, lose the save.
Online, Plaintext
You hold: a 16-char code, plus an optional 4-character key.
Server holds: the state as JSON.
Price: the server can read the save.
The key at Position 3 is a second factor, player-held only. Four letters, a–z, case-insensitive. Default is no key — the code alone loads. Set a key and you need code + key on load. The key is stored as a one-way hash; the server can verify it but cannot read it, and cannot link it to your IP, your email, or any other save.
What the server sees
| What the server sees | 01 Offline | 02 Encrypted | 03 Plaintext |
|---|---|---|---|
| Work ID | — | ✓ | ✓ |
| Save timing | — | ✓ | ✓ |
| Update frequency | — | ✓ | ✓ |
| Request IP / User-Agent | — | ✓ | ✓ |
| State size | — | ✓ | ✓ |
| Bucket write pattern | — | ✓ | ✓ |
| Save contents | — | — | ✓ |
| Key (hashed) | — | — | ✓ |
Nothing else changes between positions. The save contract is the same at every one: lose what you hold, lose the save. The lookup key is the same: sha256(slug + ':' + code). The slug prefix means the same code on two different games produces two different saves — never a collision.
The server knows the IP address of the request — that's standard server logging, documented in the privacy policy. It never stores that IP alongside the save. The save_codes table has no IP column. There is no key that joins a save to an IP, or an email to a save, or an IP to an email. They are three separate systems with no shared key. The rate limiter sees IP; the save system does not. The newsletter sees email; the save system does not. That separation is the guarantee.
Buckets
A save is not one blob. It is up to 100 buckets, each up to 4 KB, totaling at most 50 KB per code. A bucket is just a numbered slot: 0 through 99. The server does not know or care what is in each one.
This is the entire model. The developer decides that bucket 0 is player stats, bucket 3 is the level list, bucket 42 is a custom audio sequence. The server stores bytes. The semantics live in the game.
Why buckets instead of one blob
Granular writes. A game that updates the score every few seconds can write just bucket 0, not the whole save. A level editor that touches one level writes just that level's bucket. The server does less work, and the wire carries fewer bytes.
Fixed address space. Every save has 100 slots. The server never needs to know "how many buckets does this game use" — the answer is always "up to 100." No schema, no registration, no per-game configuration.
Same limits in both modes. Encrypted and plaintext saves use the same bucket model, the same caps, the same lookup key. A game is written against the same RetroSave wrapper regardless of which position the player chose.
The caps
- 4 KB per bucket. A character sheet, a level list, a config. Not an asset.
- 50 KB per code. One hundred full buckets would be 400 KB; the total budget is 50 KB, so a game that uses many buckets must keep each small.
- 100 buckets. Fixed address space. No dynamic bucket allocation.
The wrapper
The wrapper sends each bucket's payload to the server. In encrypted mode, each bucket is encrypted in the browser first; the server sees only ciphertext and a bucket id. In plaintext mode, each bucket is JSON; the server stores it and can read it.
Try It — Position 2 (Online, Encrypted)
The demo below illustrates the crypto primitives that back Position 2 — PBKDF2 key derivation, AES-GCM encryption, and a self-contained armored block. Pick a code, write some state, and click Encrypt: you'll get a real encrypted block that you can paste back to recover the state. Nothing is sent to any server. Everything happens in this tab.
The demo shows a single blob for clarity. The real save API uses buckets — see the section above — but the crypto layer is identical. The demo also does not illustrate Position 3, which is server-side plaintext with an optional word; that path has no client-side crypto to demo.
Encrypt a save
Decrypt a save
What the Server Enforces — and What It Doesn't
The server is not a game server. It stores bytes and enforces invariants that hold for every game. It never interprets your save. That line — invariants yes, interpretation no — is what makes the bucket model game-agnostic.
The server enforces
- Bound parameters. Every query uses PDO prepared statements. Your payload cannot become SQL.
- JSON content type +
nosniff. Responses are alwaysapplication/json; charset=utf-8withX-Content-Type-Options: nosniff. The browser treats the body as JSON data, never as executable HTML. - JSON validity on plaintext writes. A plaintext bucket must serialize as valid JSON or the write is rejected. This is structural, not cosmetic: a valid JSON document cannot also be a valid HTML document. Stored bytes can never become a live HTML page, regardless of what the work does with them.
- Byte caps. 4 KB per bucket, 50 KB total. No gigabyte payloads.
- Bucket range. 0 through 99. No unbounded address space.
- Rate limiting. In nginx, keyed on the TCP peer, not in PHP and not stored with the save.
- Isolation. No IP is read in PHP.
save_codeshas no IP column. Saves and rate-limits share no key.
The server does not
- Read your save. In Position 2 (encrypted), the payload is ciphertext the server cannot decrypt.
- Validate game logic. The server does not know your rules and will not learn them.
- Verify HMACs, hash chains, or honeypots. Those are client-side patterns the developer can implement using the buckets the platform provides. The platform does not enforce them and does not know they exist.
- Know what a bucket means. Bucket 0 is "player stats" only because your game says so. To the server, it is bytes.
Anti-cheat, honestly
You cannot stop a determined player from editing their own save. That's Game Genie, and for single-player games it's fine. What you can do is raise the cost from "edit the network tab" to "read the source." These are real patterns, all client-side, all built from the same 100 buckets every save has:
- HMAC with a hardcoded secret. Sign each bucket's payload with a secret in your JS. Verify on load. Defeats anyone who doesn't read the source; defeated by anyone who does.
- Hash chain in a dedicated bucket. Store a rolling hash in bucket 99. Editing any bucket breaks the chain. Detects tampering; does not prevent it.
- Shadow buckets. Store critical values twice, once with a checksum, in buckets 0 and 98. Cross-check on load.
- Honeypot buckets. Put a decoy in bucket 1. If it changed, the player was editing their save.
None of these require a change on the server. All of them are the developer's choice, implemented in the developer's JS, using the buckets the platform provides.
What a Code Unlocks
For players, a code is a save. For creators, the same code unlocks three features they don't have to build themselves:
- Cloud Saves: Up to 100 buckets of 4 KB each, 50 KB total per code.
- Per-game Leaderboards: A sortable table tied to the game, populated from player submissions.
- Multiplayer Rooms: A temporary room code a second player can use to join.
The API endpoints and the JSON sidecar fields are on the Submission Criteria.
If You Lose Your Code
⚠ The contract is the same at every position
Position 1: lose the block, lose the save. Position 2: lose the code or the block, lose the save — you need both. Position 3: lose the code, lose the save. Lose the key and the code alone won't load it.
In every case, we have no way to recover it. We don't collect an email, a username, or any personal identifier we could use to look it up. That is the entire trade-off — total privacy in exchange for total personal responsibility.
Why Not Just Use Accounts?
We've weighed it. Accounts mean password storage, email verification, password reset flows, database breaches, phishing risk, and a permanent server-side record linking every game session to a specific human being. For a collection of tiny games that mostly run in under five minutes, that's a massive amount of complexity for almost no player benefit.
A hash code sidesteps all of it. There is no identity to breach. There is no password to reset. There is nothing to phish. And the server never knows who you are — only that somebody is using a code. At Position 3 it also sees what that code's state looks like; that's the trade for not holding a block, and the exposure table above says so plainly.