Game saves, audio and a shared backend
Game infrastructure — saves that migrate, audio with no assets, a maze, and an anonymous shared backend
For when the answer to “It is a game — saves that survive updates, sound, and a shared no-accounts backend” is yes.
store-kit solves 'my data must survive a reload'; this block is the layer a GAME needs on top of that: a save whose shape can change between releases without wiping what the player earned (schemaVersion step-migration), sound that ships…
Skip the rebuild
You do not write this one. It arrives in your project as ordinary source you can read, change and keep, with its audit record and its caveats beside it.
- Download the Playground. It is free and runs on your own machine.
- Start a new app and tick “It is a game — saves that survive updates, sound, and a shared no-accounts backend”.
- This block is written in, along with anything else you ticked.
The mistake it removes
The kv-service is a PUBLIC, UNAUTHENTICATED endpoint by design — that is what 'no accounts' costs. Its defenses are real but narrow: claim tokens stop overwrites, per-IP fixed-window limits slow floods, the entry ceiling caps backend quota drain, and the injected validator is the only thing standing between you and a generic blob dump — write a strict one. What it cannot do: per-IP limits are only as good as x-forwarded-for from YOUR proxy (a caller who controls that header picks their own bucket), tokens are bearer secrets a device stores in localStorage, and nothing moderates the CONTENT of what players write — a shared store that renders player text needs its own escaping and its own moderation answer before it goes live.
What is already handled
Each of these was checked by running the code, not by reading it.
- The block's own runner (npm run test:gaming-kit) exercises the SHIPPED compiled JS, not the TypeScript source: kv-service claim-token ownership (first write claims, a wrong token gets 403 and cannot delete, tokens never appear in GET/LIST bodies), separate read/write per-IP rate limits (429 with Retry-After, one IP throttled while another proceeds), the entry ceiling (507 for a new key, existing owners can still update), save migration idempotency (migrating twice equals migrating once, steps run in order from any version, schemaVersion stamped), clamp/sanitize edges, withRetry backoff and its no-retry classes, maze BFS (stepToward's first move actually shortens bfsDistance on every cell of a seeded maze; unreachable returns null/-1), deterministic audio voices, and the config contact line staying EMPTY by default.
- Four mutants were planted while this kit was being written: the ownership check, the rate-limit comparison, the migration version guard and the BFS wall check were each broken in the shipped JS, the runner went red naming the behavior, and every file was restored byte-identical. THAT IS A RECORD OF ONE AUTHORING SESSION, NOT A CHECK THAT RUNS: nothing re-plants them, because a gate that edits shipped source in order to grade it is how a half-mutated file survives a crash, and no verdict in this repository's ledger records that set. What runs on every build is the line above — each of those four behaviors is asserted by name by the block's own runner.
- The source kit's full suite is 123 assertions (counted in the Ultimate Gaming Arena repository at the commit this block was compiled from, not counted here), and it passes there. Nothing in THIS repository runs or re-counts them; what this repository counted is the runner above.
- scripts/block-check.mjs copies the manifest's node files to a bare temp directory with no node_modules and loads the barrel there.
- Residue-grepped before packaging: no safety-core import, no moderation logic, no personal email address, no Kaiju key prefix or endpoint ships in any block file — the only Kaiju mentions are provenance comments.
What the audit found
Named rather than summarized. The reasoning behind each one ships inside the block, so it travels with the code instead of living on a page you have to trust.
- Packaged and behavior-tested, but not audited major · Deploy readiness
- The kv-service's per-IP rate limit trusts x-forwarded-for minor · Security
- Claim tokens are bearer secrets in localStorage minor · Privacy & data
- React components read as accessible but are unaudited minor · Accessibility
What you still have to do
A copied file cannot own your secrets, your host or your legal obligations. This is the part the block deliberately does not claim.
- Terminate the kv-service behind a proxy that OVERWRITES x-forwarded-for (Cloudflare, a load balancer, your platform's edge), and stand up the Upstash-style Redis backend the rate limiter counts in. Exposed directly, a caller who sets the header picks their own bucket and the per-IP limits are advisory.
- Persistence and backup settings on the Redis/Upstash backend you point the shared store at. The personal half is localStorage, which the player's browser can clear at any time — schemaVersion migration keeps a save READABLE across releases, it does not keep it alive.
- A burst-tolerant limit at your edge if the fixed-window boundary matters: the shipped limiter is fixed-window and admits up to 2x the limit across a boundary. The entry ceiling and withRetry backoff are shipped and tested.
- Claim tokens are anonymous BEARER secrets, not accounts: whoever holds the token owns the key. The client stores it in localStorage, so clearing site data orphans the player's entries (keyAvailable() fails OPEN on network errors by design — naming yourself is never blocked by an outage). There is no recovery flow; if you need one, you need real accounts, which this block deliberately is not.
- Nothing here moderates or escapes CONTENT. The kv-service validates shape and size only; if you render what other players wrote, the escaping and the moderation answer are your app's job. The value validator you inject is a load-bearing security control, not an option.
- The kv-service's backend is an Upstash-style Redis REST API (or any RedisRunner you inject). Without credentials it answers 503 'not configured' calmly rather than failing — that is deliberate — but it means the shared half of storage.js silently needs a backend you have not stood up yet.
- The rate limiter is fixed-window and lives in the backend store: correct on serverless (state survives instances), but a window boundary admits up to 2x the limit in a burst.
- audio.js needs a real AudioContext to make sound; in Node it only proves its math and envelopes. The iOS unlock must be called from a user gesture — that is a platform rule, not a kit choice.
- The react stack needs `react` >= 18 in the host app; the node stack needs nothing.
What lands in your project
node
- node/gaming-kit.js
- node/config.js
- node/storage.js
- node/kv-service.js
- node/save.js
- node/maze.js
- node/audio.js
- node/fx.js
Compiled JS (tsc, ES2022/NodeNext) from the TypeScript source — no build step needed by the consumer. Zero imports of anything: no packages, no node: builtins, no cross-file imports except the barrel's own relative re-exports. gaming-kit.js is the barrel (renamed from index.js: the scaffold flattens every node stack into server/blocks/ by basename, and challenge-kit's barrel already owns index.js there — a block-named barrel can never collide); take it or import a single module. audio.js and storage.js are browser-oriented but load and test in plain Node because their globals are injected, so they ship in this stack rather than pretending to be a second one.
react
- react/index.js
- react/modal.js
- react/banner.js
- react/hp-bar.js
- react/wheel.js
- react/odds.js
- react/theme.js
- react/use-pwa-install.js
- react/use-tilt.js
JSX compiled to plain JS (react-jsx runtime), importing only `react` and `react/jsx-runtime`. Modal (focus trap, aria-modal, Escape), Banner, HpBar, a weighted prize Wheel driven by the pure odds table in odds.js, the PWA install-prompt hook, and the device-tilt input hook. The files land side by side so the barrel's relative imports resolve after install.
Needs react >=18 from npm.
What you supply
- keyPrefix — the ONE key prefix the kv-service will touch, e.g. 'myapp:kv:' — createKvHandler refuses keys outside it
- validateValue — your value validator for the kv-service; the default caps only SIZE, so without a strict shape check the store is a public blob dump
- branding — configureKit({ appName, contactEmail, url }) — the kit ships with NO contact address anywhere; nothing renders one until the app sets it
- storagePrefixes — createStorage's personalPrefix / sharedPrefix / tokenKey, and the endpoint that switches shared data from local to the kv-service
Licensed MIT. It is a starting point, not a finished product.
Get it
Download the Playground See the other blocks
Nothing here is locked. The files are yours, in your folder, under a permissive license.
Questions
- Is Game saves, audio and a shared backend audited?
- It was audited on 2026-08-24, and the verdict was not a clean pass.
- What do I still have to do myself?
- Terminate the kv-service behind a proxy that OVERWRITES x-forwarded-for (Cloudflare, a load balancer, your platform's edge), and stand up the Upstash-style Redis backend the rate limiter counts in. Exposed directly, a caller who sets the header picks their own bucket and the per-IP limits are advisory.
- How do I get this code?
- Download the Playground, start a new app, and tick “It is a game — saves that survive updates, sound, and a shared no-accounts backend”. The block is written into your project as ordinary source you can read and edit.
All pre-built code blocks · Learn to build from zero · The coding guide