Skip to content
GameTroughSlop served daily

Docs

Hosting guide

How builds are validated, sandboxed and served — and how to package yours so it works the first time.

The short version

Zip the folder that contains index.html. Use relative asset paths. Don't load anything from a CDN.

That's about 90% of it. The rest of this page is the detail.

What happens when you upload

  1. Validation. We read the archive without extracting it and check the entry count, the uncompressed size, and the compression ratio. A ratio over 120:1 is rejected as a zip bomb. Any path containing .., a leading slash, or a drive letter is rejected outright.
  2. Executable screening. Anything with a native extension — .exe, .dll, .so, .sh and friends — fails the upload. Game Trough hosts web builds only.
  3. Prefix stripping. Most zips nest everything under one folder. We detect that common prefix and strip it, so MyGame/index.html and index.html both work.
  4. Entry detection. We look for the entry candidates your stack declares, then for any root-level HTML file, then for any HTML file at all.
  5. Content addressing. The build is written to a path derived from a hash of the archive, which is why builds can be cached aggressively and why re-uploading the same bytes is idempotent.

How your game is served

Every file comes back from /api/play/<game-id>/… with this policy:

Content-Security-Policy:
  default-src 'self' blob: data:;
  script-src  'self' 'unsafe-inline' 'unsafe-eval' blob:;
  connect-src 'self' blob: data:;
  frame-src   'none';
  object-src  'none';
  base-uri    'none';
  form-action 'none'

unsafe-inline and unsafe-eval are there because game bundles inline their boot script and WASM glue needs eval. The important line is connect-src 'self' — a malicious build has nowhere to send anything it finds.

The iframe itself is sandboxed without allow-same-origin. That forces your game into an opaque origin: it cannot read the parent page, our cookies, or anyone's session, even though it's served from the same hostname. It's the single most important thing on this page.

What that means for you

  • No CDN scripts. <script src="https://unpkg.com/..."> will be blocked. Vendor the library into your zip.
  • No remote assets. Fonts, textures, audio — bundle them.
  • No external API calls. If your game needs to talk to a server, it needs the container runtime, which isn't switched on yet.
  • localStorage is handled for you. See below — you don't have to change anything.

Saves

An opaque origin has no storage of its own. localStorage and indexedDB don't just fail to persist there — they throw on access, which is why an unguarded localStorage.getItem() used to white-screen a build.

So we replace them. Every HTML response gets a small script injected ahead of your own, which swaps in a working localStorage backed by the player's Game Trough account. Existing games need no changes: keep calling getItem/setItem, and saves follow the player across devices. Signed-out players get the same API backed by their own browser, so nothing breaks for visitors who never log in.

Writes are batched and flushed automatically when the tab is hidden or closed.

When you want more than a key-value store

window.Trough gives you real slots, labels and larger payloads:

await Trough.save({ slot: 0, data: { level: 4, hp: 12 }, label: "Level 4" });
const save = await Trough.load({ slot: 0 });   // save.data is already parsed
await Trough.list();                            // [{ slot, label, updatedAt, sizeBytes }]
await Trough.delete({ slot: 0 });

// Snapshot on a timer, plus automatically on tab-hide and page-close.
Trough.autosave(() => serializeGame(), { every: 15000 });

Trough.signedIn    // false → saves live in this browser only
Trough.quotaBytes  // total space for this game, across all slots

Every method returns a promise and rejects with a code you can branch on — quota_exceeded, too_large, rate_limited, not_found.

Limits

256 KB per game for a free account, 2 MB with Trough+, across up to 32 slots. Saves under 64 KB are restored synchronously before your first line of code runs; anything larger has to be fetched with Trough.load(), so keep the localStorage path small and put bulk state in an explicit slot.

Godot, Unity and pygbag

These never touch localStorage — they persist through Emscripten's IDBFS, which is IndexedDB. That's shimmed too, so Godot's user:// and Unity's PlayerPrefs sync to the account with no code changes. Slots and labels are available through JavaScriptBridge and .jslib respectively; there are copy-paste recipes on the saves page.

Full detail, including the explicit API and per-engine examples: /docs/saves.

Per-stack packaging

Vanilla HTML/JS

Zip the folder. Done. Keep index.html at the top.

Vite / React / Three.js

Set the base path or every asset URL will be absolute and resolve to nothing:

// vite.config.js
export default { base: "./" }

Then npm run build and zip dist/.

Godot 4 (Web)

Export with the Web preset and zip the whole export folder — index.html, .wasm, .pck, .js. We set Cross-Origin-Embedder-Policy and Cross-Origin-Opener-Policy automatically so threads and SharedArrayBuffer work.

Unity WebGL

Use Decompression Fallback or disable compression entirely. Unity's default gzip/brotli export expects the server to set specific content encodings, and the fallback path avoids that whole class of problem.

Rust / wasm-bindgen

wasm-pack build --target web, then write a small index.html that imports the generated glue. Zip both.

Size limits

Limits are per-stack, listed on the stacks page. They're generous, but the honest advice is that anything over about 15 MB loses a meaningful number of players before the first frame. In order of impact: strip debug symbols, drop unused export templates, compress textures to WebP, and make sure something appears on screen within the first 500 ms.

Updating a build

Re-upload from your game's edit page. Each upload becomes a new version with its own changelog entry; the previous build stays in storage so a bad release can be rolled back.

Games that need a server

Python backends, socket servers, anything stateful — that's the container runtime. The abstraction is implemented end to end (routing, per-game subdomains, admin surface) but no provider is connected, so those stacks are marked planned and hidden from the submit form. When it's switched on, existing submissions get picked up without any migration.

Still broken?

Open the browser console on your game page — a white screen is almost always a 404 on an asset with an absolute path. If that isn't it, post the console output in Stack Help.

Hosting guide · Game Trough