Skip to content

Docs / Start here

Builds and uploads

What a build is and what the uploader accepts. Zip or folder, limits, path rules, allowed file types, relative paths, the sandbox, caching, the builds list, retention and the storage limit.

.md

A build is one upload of your game's web files. Once it is sealed, nobody edits it, including you.

This page is the reference. For the walkthrough, see Publish a game. The limits, allowed file types and path rules here are generated from the code the uploader and the server use, and published as https://www.fradiation.games/docs/build-rules.json. If this page and that file disagree, the file wins.

What a build is

  • A set of files that forms a web game, with an entry page.
  • Uploaded at /dev/<slug> or with node check-build.mjs deploy, checked, and then sealed. A sealed build never changes.
  • Identified by a 16-character hex id. The Builds list shows the first 7.
  • Served at https://<slug>.containment.cloud/<buildId>/<path>.
  • One of a game's builds is Live. That is the one players get.

Each upload creates a new build. To change a file, upload a new build.

Zip or folder

You give the uploader either a .zip or a folder. Drop a zip on the tray or click browse. For a folder, click pick a folder. Dropping a folder doesn't work.

The browser unzips and checks the build before anything is sent. Picking the folder skips zipping and unzipping, and doesn't load the whole build into memory.

The entry file

index.html must be at the top of the build. To use another page, set entry in fradiation.json to its path, for example "entry": "game/start.html". The path is relative to the top of the build and must match a file exactly.

A URL that ends in / serves the index.html in that folder.

One top-level folder

If every file sits inside one top-level folder, that folder is dropped, once. This makes a zipped project folder work.

text
MyGame.zip
  MyGame/index.html     becomes   index.html
  MyGame/assets/a.js    becomes   assets/a.js

A second level of folders isn't dropped. If the entry file still isn't at the top, the upload stops with No index.html at the top of the build.

Junk files

These are skipped silently. They aren't uploaded and they don't count toward the limits.

  • __MACOSX folders
  • .DS_Store
  • Thumbs.db
  • desktop.ini
  • .git folders
  • Any file or folder whose name starts with ._

fradiation.json

fradiation.json at the top of the build is read for entry, viewport, threads, mutations and boards. The uploader doesn't store it as a build file, and it isn't served from your game's origin. See fradiation.json.

Limits

Sizes use binary units: 1 MB is 1,048,576 bytes, the same as the uploader.

LimitValue
Files per build1,000
Total size500 MB. Every path counts, even two paths with identical bytes.
One file200 MB
Path length240 characters
Files uploaded in partsFiles over 64 MB, automatically. Not a limit.
Stage size160 to 4096 wide, 120 to 4096 high
Patch notes2,000 characters
Mutations per upload30, of which at most 3 gamma and 1 cherenkov
Scoreboards per upload10
Uploads20 per rolling 24 hours, across all your games. Failed and cancelled uploads count.
Upload timeThe upload grant lasts 3 hours.
Cover800 by 600 JPEG made in your browser. Up to 2 MB.
Storage2 GB per developer, soft. See Retention and storage.

Path rules

Every file path in the build must follow these rules.

  • Relative, with forward slashes, and no leading slash.
  • At most 240 characters.
  • Each folder and file name starts with a letter, a digit or an underscore.
  • Each name uses only A-Z a-z 0-9 . _ space ( ) + -.
  • No name ends in a dot or a space.
  • No path appears twice.
  • The file has an extension from the allowed list.

What that rules out:

  • Dotfiles and dot folders such as .gitignore, .htaccess and .well-known/.
  • Non-ASCII names such as é.png or 日本語.png.
  • #, %, &, [, ], @, =, , and ' in names.
  • Backslashes in paths.

Paths are case-sensitive on the server. Assets/a.png and assets/a.png are different files, and a reference with the wrong case returns 404. Windows and most macOS setups ignore case locally, so a mismatch shows up only after the upload.

Spaces are allowed, but they turn into %20 in URLs. Prefer dashes or underscores.

Allowed file types

Extensions are matched without regard to case. Anything not on this list is refused, and so is a file with no extension.

KindExtensions
Pages and scriptshtml htm js mjs css
Text and datajson map txt xml
WebAssembly and engine datawasm data pck bin unityweb mem basis
Imagespng jpg jpeg gif webp avif ico svg ktx2
3D modelsglb gltf
Audiomp3 ogg wav m4a
Videomp4 webm
Fontswoff woff2 ttf otf

Refused, for example: exe, dll, zip, md, csv, bmp, tga, dds, flac, aac, opus, fbx, obj. Convert or remove them. The machine-readable list is in build-rules.json.

Pre-compressed files

Add .br (Brotli) or .gz (gzip) after an allowed extension: Game.wasm.br, Game.framework.js.gz, Game.data.br.

  • The file is served with the Content-Type of the inner extension and the matching Content-Encoding.
  • Unity's Brotli and gzip builds work with Decompression Fallback off.
  • Game.data.gz is served as application/gzip. That is Unity's Safari workaround.
  • The inner extension must be allowed. save.tar.gz is refused.
  • The server labels a file by its name and doesn't read the bytes. A .br file must really be Brotli.

Relative paths

Every build has its own URL:

text
https://<slug>.containment.cloud/<buildId>/index.html
https://<slug>.containment.cloud/<buildId>/assets/game.js

The build id changes with every upload. A path that starts with / skips it. /assets/game.js resolves to https://<slug>.containment.cloud/assets/game.js, which isn't inside any build, and the server answers 404.

Wrong:

html
<script src="/assets/game.js"></script>
<link rel="stylesheet" href="/style.css" />
<script>
  fetch("/data/levels.json");
</script>

Right:

html
<script src="assets/game.js"></script>
<link rel="stylesheet" href="./style.css" />
<script>
  fetch("data/levels.json");
</script>

The same goes for CSS url() values and anything your engine or bundler generates. In Vite, set base: "./". Other bundlers have an equivalent public path setting. Never hard-code the slug or the build id.

Absolute URLs to other origins are fine, for example an API. With threads on, they need extra headers. See Threads.

The frame

Your game runs in a sandboxed iframe on its own origin. It loads after the player clicks Start. Restart reloads it.

html
<iframe
  sandbox="allow-scripts allow-same-origin allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-modals allow-orientation-lock"
  allow="fullscreen; gamepad; autoplay; accelerometer; gyroscope; xr-spatial-tracking"
></iframe>

Every file a game origin serves also carries this header:

text
Content-Security-Policy: frame-ancestors <Fradiation's own origins>; base-uri 'self'; form-action 'none'; object-src 'none'

allow-same-origin gives the game its real origin, so localStorage and IndexedDB work. Unity PlayerPrefs and Godot user:// saves depend on that. It is safe because the game's origin is a different site from Fradiation.

What your game can do:

  • Run scripts, WebAssembly and WebGL.
  • Use pointer lock, fullscreen, gamepads, motion sensors and XR.
  • Open popups, and use alert, confirm and prompt.
  • Lock the screen orientation.
  • Call external APIs with fetch.
  • Store data on its own origin.
  • Talk to the site through the SDK.

What it can't do:

  • Touch the main site. It is on another origin, and its only channel is the SDK.
  • Navigate the page around it. allow-top-navigation isn't granted.
  • Submit forms. allow-forms isn't granted, and the CSP sets form-action 'none'.
  • Load plugins (object-src 'none') or point <base> at another origin (base-uri 'self').
  • Be embedded on another site. frame-ancestors allows only Fradiation.
  • Use the camera, the microphone or geolocation. They aren't in the allow list.
  • Count on downloads. allow-downloads isn't in the sandbox list, so browsers can refuse a download the game starts.

Storage

Each game has its own origin, https://<slug>.containment.cloud. Its localStorage, IndexedDB and Unity cache belong to that origin.

  • No other game can read them, and neither can the site.
  • All of a game's builds share them, because the origin doesn't change with the build. Saves survive updates. Keep your save format readable by newer and older builds.
  • The slug is permanent, so the origin is too.

Viewport

The cabinet letterboxes the game to the aspect ratio of viewport. Set viewport in fradiation.json, or enter a stage size in the uploader. It is the game's native resolution.

Make the game fill its window, 100% wide and 100% high, and handle resize. Don't hard-code a canvas size. Players can also go fullscreen.

html
<style>
  html,
  body {
    margin: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: #000;
  }
  canvas {
    display: block;
    width: 100%;
    height: 100%;
  }
</style>

Threads

"threads": true is accepted and already changes how the build is served, ready for when the game pages are isolated:

  • The build is served under https://<slug>.containment.cloud/coi/<buildId>/….
  • Files carry Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp.
  • Everything the game loads from another origin, scripts, fonts, images and fetch calls, must send CORP or CORS headers. Bundle everything instead.
  • Files from your own build already send Cross-Origin-Resource-Policy: cross-origin.
  • The setting belongs to each build. Leave it false unless the engine needs threads.

Caching

  • HTML files (html, htm) are sent with Cache-Control: no-cache and revalidated on every load.
  • Every other file is sent with Cache-Control: public, max-age=31536000, immutable, a year.
  • A new build has a new URL, so an update never fights a cache.
  • You can't patch a file in a sealed build. Upload a new build.

Content-addressed uploads

Only files the game doesn't have yet are uploaded.

  1. The uploader (the browser uploader on the game's Manage page, or check-build.mjs deploy) reads every file and computes its SHA-256.
  2. It sends the site the file list: paths, sizes and hashes.
  3. The site answers with the hashes the game doesn't have.
  4. The uploader sends only those files, straight to the play server, four at a time. Files over 64 MB go up in parts.
  5. The site checks that every file arrived at its full size, writes the build's file list and seals the build.

What follows from it:

  • A file's identity is its bytes. Renaming a file doesn't upload it again, and two paths with identical bytes are stored once.
  • Bytes that don't change aren't sent. A hashed bundler filename such as index-a1b2c3.js is fine. Anything that changes a file's bytes, such as an embedded build timestamp, sends it again.
  • Storage is per game. Nothing is shared between games.
  • The Builds list shows what each upload sent: (1.6 KB new) or (nothing new). The uploader shows how much was left unchanged, and so does deploy (newFiles and uploadedBytes with --json).
  • The 500 MB limit counts every path. Storage counts each distinct file once per game.

The builds list

The Builds panel on /dev/<slug> lists every build, newest first: the first 7 characters of its id, date, file count, size, how much was new, stage size and patch notes.

TagMeaning
LiveThe build players get. A draft's live build is always its newest.
TestingThe build a playtest serves.
PinnedIts files are kept whatever retention says.
UploadingThe upload hasn't finished.
ActionWhat it does
OpenOpens the build directly on its origin, outside the cabinet. The SDK has no site to connect to there.
TestSends the build to your playtest. Shown when a playtest exists.
Make liveMakes the build the live one. That is how you ship an update and how you roll back.
PinKeeps its files. Click again to unpin.
Hold to delete filesDeletes the build's files now. See below for when it appears.

Any sealed build that still has its files can be made live. Players get it on their next load. A build whose files were deleted can't be made live, sent to testers or opened. Upload it again as a new build.

From a terminal, node check-build.mjs builds --game <slug> prints the same list, and live <buildId> and playtest <buildId> do what Make live and Test do. See Deploy from the command line.

Retention and storage

There is no build quota. Old builds lose their files on a schedule, and their rows stay.

What keeps its files

For each game:

  • The live build.
  • The build its playtest serves, even while the playtest is closed.
  • Pinned builds.
  • The 5 newest builds that still have files.

Everything else

Any other build is scheduled for deletion. Its files are deleted 30 days later.

  • The 30 days count from when the site first notices the build is no longer needed. That happens after an upload, Make live, a change to the playtest build or a pin, and when you load /dev or /dev/<slug>. A late notice only makes the wait longer.
  • The Builds list shows Files will be deleted <date>. Pin it to keep them.
  • If the build becomes needed again before then, the clock resets.
  • When the files go, the build's row, patch notes, playtest feedback and judgment history stay. The list shows Files deleted <date>.
  • A file shared by several builds stays until the last build that uses it loses its files.
  • Failed and abandoned uploads are deleted with their files on the next sweep. An upload left open for 4 hours is marked failed.

Storage limit

The soft limit is 2 GB per developer.

  • It counts every build that still has files, including uploads in progress. Each distinct file counts once per game. Covers aren't counted.
  • One upload may cross the limit.
  • Once your total is at or over 2 GB, the next upload first deletes the files of builds already scheduled for deletion, without waiting for the 30 days.
  • The upload is refused only if you are still at or over the limit: Your builds use X of 2 GB. Delete or unpin some to upload more.
  • At 80% the game's page warns you. Every build that isn't live or testing then gets Hold to delete files. Before that, it appears only on builds already scheduled for deletion.
  • Deleting files removes the pin. It doesn't work on the live build or the testing build.

/dev shows a storage meter and each game's size.

Check a build first

check-build.mjs is a zero-dependency Node 20 or later script. It checks a build against the real rules on your machine.

bash
curl -fsSL https://www.fradiation.games/skills/fradiation/check-build.mjs -o check-build.mjs
powershell
Invoke-WebRequest -UseBasicParsing https://www.fradiation.games/skills/fradiation/check-build.mjs -OutFile check-build.mjs
bash
node check-build.mjs <folder-or-zip>
node check-build.mjs <folder-or-zip> --json
node check-build.mjs pack <folder> [out.zip]
  • The first command checks a folder or a zip.
  • --json prints a machine-readable report, for agents. It can go anywhere in the command.
  • pack takes a folder. It checks it, and zips it the way the uploader expects only if the check passes. Without out.zip, it writes <folder>.zip next to the folder. It won't write the zip inside the folder.
  • Exit code 1 means the build would be refused.

With an API token in FRADIATION_TOKEN, the same script uploads:

bash
node check-build.mjs deploy <folder-or-zip> --game <slug> [--live] [--playtest] [--notes "..."]

It runs the check first and uploads nothing if the check fails. See Deploy from the command line.

The script is also in the agent skill at https://www.fradiation.games/skills/fradiation/SKILL.md. See Working with agents.

Zipping

The uploader refuses a zip whose entry names contain backslashes. Windows PowerShell 5.1 Compress-Archive can write them:

text
assets\a.js: paths must be relative, with forward slashes

Use one of these instead:

  1. Pick the folder directly in the uploader. There is no zip.
  2. node check-build.mjs deploy <folder>. It uploads the folder with an API token. There is no zip.
  3. node check-build.mjs pack <folder>. It checks the build and zips it.
  4. tar on Windows 10 or later, and on macOS. Both ship bsdtar, which writes zips.
bash
tar -a -c -f build.zip -C <folder> .
  1. On macOS or Linux, Info-ZIP:
bash
cd <folder> && zip -r ../build.zip .

Errors

MessageCause and fix
No index.html at the top of the build.The entry file isn't at the top, after the one top-level folder is dropped. Move it, or set entry.
<path>: paths must be relative, with forward slashesA backslash or a leading slash. See Zipping.
<path>: file type not allowedThe extension isn't on the list, or there is none.
<path>: "<name>" isn't an allowed file or folder nameA name starts with a dot, uses a character outside the allowed set, or ends with a dot or a space. Rename it.
<path> is in the build twice.Two entries in the zip have the same path.
<path> is over 200 MB.One file is over the limit.
<n> files; the limit is 1000.Too many files.
<size>; the limit is 500 MB.The build is too big.
There's nothing in it.No files after junk is skipped.
That zip couldn't be read.The file isn't a zip, or it is damaged. See Zipping.
fradiation.json isn't valid JSON.A syntax error. JSON has no comments and no trailing commas.
fradiation.json: <field>: <message>A field breaks the schema. See fradiation.json.
Twenty uploads in a day is the limit. Try again tomorrow.The daily upload limit.
Your builds use X of 2 GB. Delete or unpin some to upload more.The storage limit. See above.
<n> files didn't arrive intact (<paths>). Upload the build again.The connection dropped, or a file was cut short. Upload again.
That build's files were deleted. Upload it again.The build was past retention. Upload it as a new build.
That token doesn't exist., was revoked or expired on <date>deploy and the API only. Make a new token under Agents & tokens (/dev/agents) and set FRADIATION_TOKEN again.
This token only works for <slug>.A one-game token was used for another game. Leave out --game, or use a token for that game.
Pass --game <slug>. This token works for all your games: ...Say which game.
Start a playtest first.playtest <buildId> on a game with no playtest. Start one on the game's Manage page. (deploy --playtest still uploads, and warns.)
That upload isn't open any more. Start it again.The upload was finished, cancelled or abandoned (open for over 4 hours). Deploy again.