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.
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 withnode 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.
MyGame.zip
MyGame/index.html becomes index.html
MyGame/assets/a.js becomes assets/a.jsA 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.
__MACOSXfolders.DS_StoreThumbs.dbdesktop.ini.gitfolders- 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.
| Limit | Value |
|---|---|
| Files per build | 1,000 |
| Total size | 500 MB. Every path counts, even two paths with identical bytes. |
| One file | 200 MB |
| Path length | 240 characters |
| Files uploaded in parts | Files over 64 MB, automatically. Not a limit. |
| Stage size | 160 to 4096 wide, 120 to 4096 high |
| Patch notes | 2,000 characters |
| Mutations per upload | 30, of which at most 3 gamma and 1 cherenkov |
| Scoreboards per upload | 10 |
| Uploads | 20 per rolling 24 hours, across all your games. Failed and cancelled uploads count. |
| Upload time | The upload grant lasts 3 hours. |
| Cover | 800 by 600 JPEG made in your browser. Up to 2 MB. |
| Storage | 2 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,.htaccessand.well-known/. - Non-ASCII names such as
é.pngor日本語.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.
| Kind | Extensions |
|---|---|
| Pages and scripts | html htm js mjs css |
| Text and data | json map txt xml |
| WebAssembly and engine data | wasm data pck bin unityweb mem basis |
| Images | png jpg jpeg gif webp avif ico svg ktx2 |
| 3D models | glb gltf |
| Audio | mp3 ogg wav m4a |
| Video | mp4 webm |
| Fonts | woff 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-Typeof the inner extension and the matchingContent-Encoding. - Unity's Brotli and gzip builds work with Decompression Fallback off.
Game.data.gzis served asapplication/gzip. That is Unity's Safari workaround.- The inner extension must be allowed.
save.tar.gzis refused. - The server labels a file by its name and doesn't read the bytes. A
.brfile must really be Brotli.
Relative paths
Every build has its own URL:
https://<slug>.containment.cloud/<buildId>/index.html
https://<slug>.containment.cloud/<buildId>/assets/game.jsThe 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:
<script src="/assets/game.js"></script>
<link rel="stylesheet" href="/style.css" />
<script>
fetch("/data/levels.json");
</script>Right:
<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.
<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:
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,confirmandprompt. - 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-navigationisn't granted. - Submit forms.
allow-formsisn't granted, and the CSP setsform-action 'none'. - Load plugins (
object-src 'none') or point<base>at another origin (base-uri 'self'). - Be embedded on another site.
frame-ancestorsallows only Fradiation. - Use the camera, the microphone or geolocation. They aren't in the
allowlist. - Count on downloads.
allow-downloadsisn'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.
<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-originandCross-Origin-Embedder-Policy: require-corp. - Everything the game loads from another origin, scripts, fonts, images and
fetchcalls, 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
falseunless the engine needs threads.
Caching
- HTML files (
html,htm) are sent withCache-Control: no-cacheand 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.
- The uploader (the browser uploader on the game's Manage page, or
check-build.mjs deploy) reads every file and computes its SHA-256. - It sends the site the file list: paths, sizes and hashes.
- The site answers with the hashes the game doesn't have.
- The uploader sends only those files, straight to the play server, four at a time. Files over 64 MB go up in parts.
- 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.jsis 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 doesdeploy(newFilesanduploadedByteswith--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.
| Tag | Meaning |
|---|---|
| Live | The build players get. A draft's live build is always its newest. |
| Testing | The build a playtest serves. |
| Pinned | Its files are kept whatever retention says. |
| Uploading | The upload hasn't finished. |
| Action | What it does |
|---|---|
| Open | Opens the build directly on its origin, outside the cabinet. The SDK has no site to connect to there. |
| Test | Sends the build to your playtest. Shown when a playtest exists. |
| Make live | Makes the build the live one. That is how you ship an update and how you roll back. |
| Pin | Keeps its files. Click again to unpin. |
| Hold to delete files | Deletes 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
/devor/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.
curl -fsSL https://www.fradiation.games/skills/fradiation/check-build.mjs -o check-build.mjsInvoke-WebRequest -UseBasicParsing https://www.fradiation.games/skills/fradiation/check-build.mjs -OutFile check-build.mjsnode 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.
--jsonprints a machine-readable report, for agents. It can go anywhere in the command.packtakes a folder. It checks it, and zips it the way the uploader expects only if the check passes. Withoutout.zip, it writes<folder>.zipnext 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:
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:
assets\a.js: paths must be relative, with forward slashesUse one of these instead:
- Pick the folder directly in the uploader. There is no zip.
node check-build.mjs deploy <folder>. It uploads the folder with an API token. There is no zip.node check-build.mjs pack <folder>. It checks the build and zips it.taron Windows 10 or later, and on macOS. Both ship bsdtar, which writes zips.
tar -a -c -f build.zip -C <folder> .- On macOS or Linux, Info-ZIP:
cd <folder> && zip -r ../build.zip .Errors
| Message | Cause 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 slashes | A backslash or a leading slash. See Zipping. |
<path>: file type not allowed | The extension isn't on the list, or there is none. |
<path>: "<name>" isn't an allowed file or folder name | A 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. |