Zip format, versions, and rollback
What your zip needs to contain
index.htmlat the zip root is required — that's the entry point ShortPlay loads (BUNDLE_ENTRY = 'index.html',api/src/upload.ts:65). A zip with no rootindex.htmlis rejected at upload withbundle_missing_index_html(api/src/upload.ts:292).- 50 MB max, checked twice — once on the object as stored, once again while the archive is
being expanded (an archive that would expand past the limit is rejected mid-read, not just at
the outer file size —
api/src/upload.ts:275-276, 355-356). - Everything your game needs — code, art, audio, fonts — ships inside the zip. Once uploaded, your game's CSP won't let it fetch external assets at runtime (see Hosted-game contract → sandbox/CSP posture), so nothing can be loaded from a CDN at play time.
- Optional: a
shortplay.jsonat the zip root with your capability manifest (inputs,orientation, etc.) gives the upload wizard's auto-detect step a head start — without one, ShortPlay heuristically scans yourindex.htmlfor signals (keyboard/mouse/touch keywords) as a starting guess. Either way, you confirm (and can correct) the manifest in the upload wizard's Confirm step before the draft is created — auto-detection is a starting point, never the final word (api/src/upload.ts:187-267).
The upload flow, endpoint by endpoint
POST /v1/uploads/init— start an upload for a game (title + hook + tag); returns anuploadIdand auploadUrlto PUT your zip to. Rate-limited to 10 uploads per account per hour (api/src/upload.ts:64,api/src/upload-routes.ts:268-329).PUT /v1/uploads/:id/bundle— stream your zip body to that URL (api/src/upload-routes.ts:335-367).POST /v1/uploads/:id/complete— send your confirmed manifest (+ title/hook/tag); the bundle is validated (zip structure,index.htmlpresence, size) and a draft game/version row is created. Nothing is published yet (api/src/upload-routes.ts:374-).POST /v1/games/:id/publish— promotes a draft (or a specific version, via{ "version": N }in the body) to live. This is the only step that makes a game appear in the feed, and only after the next pool-rebuild cron. Publish also enforces a completeness gate: a game with no hook or no genre tag set in Details is refused with422 details_required— the feed never shows an incomplete listing (api/src/upload-routes.ts:612-786).
Versions
Every upload after the first becomes a new version on the game's ladder — versions are immutable once promoted, and only one version is ever live at a time. You can:
- Publish a specific version —
POST /v1/games/:id/publishwith{ "version": N }promotes that version (or, if it was already live before, re-promotes it) (api/src/upload-routes.ts:647-693). - Roll back —
POST /v1/games/:id/rollbackwith{ "version": N }re-pointslive_versionto a prior version. Rollback does no R2 writes (versions are immutable), so it's instant (api/src/upload-routes.ts:788-824). - See the version history —
GET /v1/games/:id/version-historyreturns every version, its state (draft/published), whether it's the currently-live one, and its release notes (api/src/upload-routes.ts:826-893).
Release notes
Each version can carry release notes (max 2000 characters,
api/src/upload-routes.ts:1586 MAX_RELEASE_NOTES_LEN), attached at completion time and surfaced
back in the version-history response. They're free text — a changelog line for players or your own
record, not machine-parsed.