opengame.json must look like, and what each check reports when they don’t.
Before you start
Install the CLI through the Quickstart, then run this from your game folder:What we accept
Static browser exports from any engine are eligible for desktop play. Single-thread and multi-thread exports both run. The scan puts a multi-thread one on an isolated contract, and the build stays as it is. Test it and any WebGPU build on the private preview, which threads and WebGPU covers. An engine’s name in your source code does not make the game compatible. A runtime check and a reviewer assess the exported build, and Engine support shows how far each engine has got.Engines identified by the checks
Engines identified by the checks
Godot, Unity, Construct, GDevelop, Defold, GameMaker, RPG Maker, Cocos Creator, ct.js, LÖVE (love.js), Ren’Py, Solar2D, Kaigen, Bevy, Ebiten, KNI, MonoGame, Stride, Flutter, Flame, GB Studio, PICO-8, js-dos, Ruffle, Three.js, Babylon.js, Phaser, PixiJS, PlayCanvas, Excalibur, KAPLAY, LittleJS, melonJS, React, Heaps.io, raylib, Emscripten, .NET, Pyodide, wasmoon, Haxe, Rust, Go, Zig, Swift, C, C++, C#, Python, Lua, JavaScript, TypeScript, Custom.
- Engine never fails a build. It names the engine and version it finds, and flags the game for review when runtime compatibility is not established. The exported build still needs a playtest.
- Portrait fails a game that passes
"portrait"toscreen.orientation.lock, and flags one for review when a fixed canvas is taller than it is wide. - Touch-only never fails a build. It flags a game for review when it has touch listeners and no keyboard or mouse listeners, and a person checks, because a bundler can hide listeners.
Lay out the folder
A browser export needsindex.html at the root of the output folder. If the root has none and there is exactly one deeper down, the CLI serves from that folder and prints a root-stripped notice. If there are several deeper down, the check fails and lists them, and you point build.output at the game’s folder.
A game that is one .swf or .jsdos file can upload without an index.html if you deploy it with --vendor: the check reads the file whole, and the CLI adds a pinned player and a page for it, as the Ruffle and js-dos guides describe. Renaming another file to .swf or .jsdos does not do this.
A source project does not become a web export by deploying it. The engine export workflow covers choosing an output, local runtime experiments, vendoring and failure codes.
The CLI leaves these out of the upload, and a
files_excluded notice lists each one:
node_modules/and.git/.envfiles- JavaScript, CSS and WebAssembly source maps
- coding-agent folders:
.claude,.agents,.codex,.cursorand.opencode - operating-system files:
.DS_Store,Thumbs.db,desktop.ini,.Spotlight-V100,__MACOSX, and any file whose name starts with._
opengame.json, .opengame/ and opengame-assets/ at the top of the folder are left out too, with no notice, because they belong to the project, not the game players download. A .opengame/ folder further down is left out with a files_excluded notice.
Paths must match letter case on disk. macOS and Windows ignore case, and our servers do not. Every path must stay inside the folder, with no ../, symlinks or absolute paths. The CLI stops with a symlink error on a symlink, and with unicode_collision on two names that differ only in how an accented letter is written.
A folder that looks like source, with a package.json, a src/ folder or .ts files and no index.html, fails. Run your build and point build.output at what it produces.
Upload a large game
A decompilation, a mod or a recompilation can include gigabytes of the original game’s data, far over the limits above. A game like that can send its large files separately from the archive, on thelarge-game-v1 plan: up to 12 GiB in 32,768 files, and 1 GiB for any one file. Pages, scripts, stylesheets and wasm stay in the archive, within the ordinary limits. All files are scanned and served at the same path as the rest of the game, and a player downloads only the files the game requests.
Before the first upload, ask the OpenGame team to enable large-asset access for your account. Without it, the deploy fails before any file is sent. There is no publication statement to accept on this plan.
Write an asset plan and use it for both commands:
large-game.json
"assets": "auto", the CLI chooses the files. Files of 1 MiB or more go, then the largest of the rest, one at a time, until the archive fits 200 MiB and 3,000 files. HTML, JavaScript, CSS, XML, SVG, wasm, web manifest, SWF and PDF files always stay in the archive, compressed or not, and so do opengame.json and the cover. Smaller files stay too, so opengame check reads them before upload: a config file that names an outside host is found on your machine. check prints how many files it chose. The same export always gives the same plan, so an interrupted deploy resumes where it stopped.
To choose the files yourself, list them, or folders ending in /, in place of "auto": "assets": ["main/", "sound/music.pak"].
If the archive is still too big with all eligible files out, check fails and lists the largest files left. Those are code, so the fix is a smaller build.
A Kaigen build uses its own plan and cannot use this one.
Write opengame.json
opengame init writes this file. Commit it with your code. The website edits the same fields, and Update a live game covers what happens when both change.
opengame.json
title: Required. 3 to 40 characters.description: Required. 40 to 500 characters. What the game is and how it plays.tagline: Optional. Up to 140 characters.genre: Required. One slug, such asshooter-survival,platformerorpuzzle-casual.opengame initandopengame checklist them all when the genre is missing or wrong.kind: Optional. What you are putting on OpenGame. Leave it out and it is agame;opengame initalways writes it. One of:game: something to play, with goals, rules or a way to win.world: a place to explore or hang out in, with nothing you have to win.decomp: an existing game rebuilt from its decompiled code.mod: changes to an existing game, like new levels, rules or art.
basedOn: Required for adecompor amod, and not allowed for agameor aworld. The name of the game it is made from, up to 60 characters. The game page shows it as “Mod of Doom”. Only games and worlds can enter the jam.controls: Required. Any ofkeyboard,mouse,gamepad, each listed once.requires: Optional. What the game needs from the browser:renderer(webgl,webgl2orwebgpu), andthreads,desktopOnlyandgamepadas true or false.build.command: Optional. Runs on your machine beforedeployunless you pass--no-build, and beforecheck --build. It is never uploaded.build.output: Required whenbuildis set. The folder to upload, such asdist. Use.for the folder itself.cover: Optional. A path relative toopengame.json. The CLI adds it to the upload at the same path even when it is outsidebuild.output, with acover_includednotice.multiplayer: Optional.modeisnoneorp2p, andmaxPlayersis required: 1 fornone, 2 to 16 forp2p. Ap2pgame can addlobbyfor room settings, which the multiplayer guide covers.leaderboards: Optional. Up to 10. See Add a leaderboard.achievements: Optional. Each has anid, atitleand an optionaldescription, within the achievement limits.deploysaves them when the private preview is ready.sdk: Optional. The SDK version range, or an object withinjectand optionalversion. Settinginjectto false disables automatic global injection on releases that provide it. See Services from any engine.runtimes: Optional. Pinned platform-runtime selectors and supported shell options. Use the exact version in the CLI’s runtime catalog. Platform runtimes shows how to list them.
manifest check. The one exception is $schema, which the check accepts when it names OpenGame’s own schema; you don’t need it. If a newer CLI wrote it, the finding tells you to run opengame upgrade.
The game’s ID goes in .opengame/project.json, which init adds to .gitignore, so a fork of your repository cannot deploy over your game. Jam answers belong to one entry, so they are not in this file either.
A credential anywhere in opengame.json, including in build.command, fails the deploy. Use environment variables there instead.
What a running game may reach
A game uploaded now runs on runtime contract 2027-03, or 2027-04 when it uses threads. It can reach its own origin, OpenGame’s API at https://api.opengame.gg and the multiplayer broker at wss://rtc.opengame.gg and https://rtc.opengame.gg, and any other host overhttps: or wss:. That covers:
- calls to an AI model, such as a character the player talks to, or any other third-party API
- your own game server, including your own WebSocket server for multiplayer
- scripts, stylesheets, fonts, images and media from a CDN
opengame check lists every outside host the game names in an external-hosts note, which a reviewer sees too. You don’t need to change anything for it.
Before the game calls out, know these:
- The player’s IP address. Each call goes from the player’s browser, so that host gets the player’s IP address.
- Offline review. The review sandbox does not reach the internet. The game must still draw its first frame when its server is out of reach.
- https only. The game page is served over
https:, so the browser blockshttp://andws://requests. A server without TLS can’t be reached from a published game. - Threads. A build that uses threads runs cross-origin isolated on 2027-04. There a script or image from another host loads only if you request it with
crossoriginand the host sends CORS headers, or if the host sends aCross-Origin-Resource-Policyheader.
fetch("/api/..."), because it stays on the game’s origin. There is no server behind that origin, so the request returns 404. Call your server by its full https:// address.
Multiplayer can use the OpenGame broker, which the multiplayer guide covers, or your own server.
What a game may not do
We refuse these whatever host the game uses.opengame check, the upload scan and the review sandbox look for the common services and code, and a reviewer plays every game.
- Payments. Games on OpenGame are free to play. A game may not take money, tips, donations or card details, or send players to a page that does.
- Crypto. Games on OpenGame may not use crypto wallets, tokens, NFTs or blockchains, and may not mine.
- Accounts. Players start playing without making an account, signing in, or giving an email address. OpenGame gives every game a player through the SDK, so a game never needs its own sign-in. An anonymous sign-in to your own backend, such as Firebase anonymous auth, is fine, because the player is never asked for anything.
- Ads. Games on OpenGame may not show ads. A portal SDK such as Poki’s or CrazyGames’ serves ads, so remove it.
- Tracking. Games on OpenGame may not carry ad pixels or session recording. Use the SDK’s events for analytics.
og.openLink. We do not open links to the payment, crypto and sign-in services on our list, judged by the address you pass, not by where that address redirects. Limits lists the services the checks look for.
Keep credentials out
A file containing something shaped like a key or token fails the check. Anything in a bundle is public, so a key in a game is a key on the internet. If the value was real, rotate it. To call an API that needs a key, put the key on your own server and have the game call that. Identity, scores and lobbies come from the page through the SDK, so those need no key.What the checks find
opengame check, and a deploy before it uploads, report each problem as a finding with one of the names below in its check field. After the upload, the server reports the same problem under a broader check name, such as preflight, secrets, engine or desktop, with the same sentence in evidence.observed.
Most findings give the file and line in evidence.file. missing-ref does not: it is one finding for every missing file. Its evidence gives the count, and its fix names up to ten of them after Not found:, each written as file:line → missing path.
Checks that only apply to one engine’s exports, such as engine-cdn-runtime or godot-pwa, are in the export workflow.
Folder and entry
empty: The folder has no files in it. Run your build, then pointbuild.outputat what it made.no-entry: There is noindex.htmlanywhere in the build folder, and no SWF or.jsdosfile OpenGame can make a page for. Export the game for the browser, or follow the Ruffle or js-dos guide.root-ambiguous: The build has noindex.htmlat the top and more than one further down. The finding lists them. Pointbuild.outputat the folder with the game’sindex.html.source-not-build: The folder has source code in it, not a build. Run your build and pointbuild.outputat what it makes.forbidden-dir: An upload made without the CLI includesnode_modules/or.git/. Remove them, or upload with the CLI, which leaves them out.file-countanduncompressed-size: The build has too many files, or is too big unpacked. The CLI stops before this with abundle_limiterror. Shrink the build to fit the limits.size-warn: A warning only: the unpacked build is over 50 MiB. It still uploads.unreadable-file: OpenGame could not read a file. Finish the build, check you can open the file, and run the check again.
size-warn, the playtest run at submit measures the bytes loaded before the first playable frame. Optimise your assets has ways to shrink both.
File names and paths
case-mismatch: A reference and the file it names differ in capitals, such asRock.pngandrock.png. Make them match.case-collision: Two file names differ only in capitals. macOS and Windows treat them as one file. Rename one.unicode-collision: Two file names look the same, but one writes an accented letter as one character and the other as two. Keep one. The CLI stops before this with aunicode_collisionerror, so you see this finding only on an upload made without it.name-controlandname-bidi: A file name contains an invisible control or text-direction character. Rename the file.path-too-long: A path in the game is too long to store. Shorten the folder or file names.
References and network
missing-ref: A warning: the HTML, CSS or JavaScript asks for a file that is not in the build. Add the file, unless the game creates it while it runs. The SDK’s own read ofopengame.json, written asnew URL("./opengame.json", location.href), is never reported: it happens only when the game runs outside OpenGame. A plainfetch("opengame.json")in your script is still reported.missing-import: A script imports a module that is not in the build. Add the module, or bundle it into the script.bare-import: A module imports a package by name, such asimport "three", with no local path. Bundle the dependency, or map the name to a local file in the page’s import map. For Vite, build the project and uploaddist.missing-module-path: An import resolves to a path that is not in the build. Use a path relative to the importing module, or rebuild so the dependency is included, and check the bundler’s base path setting.outbound-origin: The game requests a host over plainhttp://orws://, which the browser blocks on anhttps:page. Change it tohttps://orwss://. A plain<a href>to another site is only a warning: nothing is fetched, but clicking it does nothing in the game frame. For a link a player should follow, callog.openLinkwith the address.blocked-service: The game reaches or uses a service on the list of what a game may not do, such as a payment provider, a wallet or an ad network. The finding names the service and the rule. Remove it.review-signal: A warning: the game has an email or password field. It does not stop the upload. A reviewer checks it is not a sign-up wall; a save name or a room password is fine.external-hosts: A note listing the hosts outside OpenGame that the game names. Nothing has to change. The game should still start when one of them is slow or down.ice-server: A warning: the game names a STUN or TURN server outside OpenGame, which would send players through someone else’s relay. Remove it and use the multiplayer broker. If it is a library default your game overrides, such as PeerJS’s own servers, you can leave it.import-map-remote-target: An import map sends a name to a module outside the game: another host, adata:orblob:URL, or a bare name. The check reads each target in each map, imported or not. Map it to a file inside the game, or bundle it with your build tool.base-ambiguous: A page, or an iframesrcdocinside one, has more than one<base href>, so the check cannot tell which base a URL uses. Keep one<base>in the<head>, or remove it and use relative paths.srcdoc-too-deep: A page nests iframesrcdocdocuments five or more deep, so the check cannot read the inner pages. Move the inner page into a separate HTML file and load it withsrc.
Credentials in files
credential: A file looks like a key or a credential store. Remove it, and rotate the key if it was real.credential-in-file: A file contains a credential. Remove it, and rotate the key if it was real.encoded-credential: A credential is hidden in an escaped string or a compressed file. Remove it, and rotate the key if it was real.possible-secret: A warning: something looks like a secret being set. If it is one, remove it.
Archives, engine payloads and large assets
engine-archive-structure: Agame.loveor a Ren’Pygame.zipdoesn’t hold what its engine loads first:main.luaorconf.luaat the top of a.love, or a.rpy,.rpycor.rpafile undergame/in a Ren’Py zip. Any other zip is refused as a nested archive (engine_archive_structure). Upload the archive your engine exported, or unpack the files into the folder.nested-archive: The game contains an archive, such as a ZIP, that the checks cannot open. Some engines’ own archive formats can be opened, but renaming a file does not make it one. Read the finding before you repack anything.godot-pck-encrypted: The Godot pack (PCK v2 or v3) has an encrypted directory. The malware and credential scans cannot read encrypted game data, so OpenGame rejects the deploy withengine_archive_encrypted. The same goes for a pack with encrypted files inside it, and for an encrypted Defold resource. Export it without encryption.engine-archive-not-inspected: A warning: part of a SWF or.jsdosfile could not be decoded. It is still scanned, and the reviewer sees it marked. OpenGame makes a page only for a file that decodes completely, so without your ownindex.html, rebuild the file.engine_archive_codes, such asengine_archive_truncated,engine_archive_invalid_headerorengine_archive_byte_limit: A SWF or.jsdosfile is damaged, or bigger than the limits once unpacked. The check fails. Rebuild or unpack the file, then check again.compressed-asset-mismatch: A.bror.gzcopy does not match the file beside it, such asmain.js.brandmain.js. Some browsers get the compressed copy, so make it again from the original, or delete it.external-reference-path-invalid: The list of large-asset paths is malformed, too long, or names paths that cannot be used. Make it again, with paths relative to the export folder.external-assets-invalid: The large-asset plan does not match the export’s files, paths or sizes, or goes over a limit. Make the plan again from the current export, and shorten long paths.external-assets-unity-required: Theunity-assets-v1plan needs a complete Unity browser build. Export the whole Unity build again and make its asset plan again.external-assets-engine-required: The asset plan needs a complete, fully inspected boot tree from an eligible engine. For a Kaigen large build, use its own plan;large-game-v1excludes Kaigen.external-assets-licence-required: Files were moved outside the archive for a licensed engine, and no boot.wasmcontains that engine’s licence stamp. Upload the build the engine produced, with its.wasmunchanged.engine-licence-stamp: A note. The build contains an engine’s licence stamp, which is normal for Kaigen web builds. It never fails the check. Leave the.wasmas the engine built it; publishing asks you to accept the engine’s publication statement.licence-search-incomplete: A note. Some files could not be searched for an engine licence stamp in the time the check allows. The server’s scan reads them again.
Layout and images
platform-desktop: The game may not fit a desktop window. A portrait lock fails; anything else goes to a reviewer. See what we accept.image_format_unsupported: The image check cannot read this format:.heic,.heif,.jxl,.psd, or a KTX1 (.ktx) texture in a GPU format it has no decoder for, such as ETC or PVRTC. It reads KTX2, and KTX1 in BC1 to BC5, BC7, ASTC LDR or plain pixels. Convert the image to PNG, JPEG, WebP or KTX2.
Manifest and build
manifest: A field inopengame.jsonis missing or invalid. Fix the field the finding names. Each bad field gets its own finding.build: The build command failed or ran out of time. Run it yourself to see the error.
Notices
These come back asnotice lines, not findings, because they report a choice the CLI made or something that needs no change. None of them stops a deploy.
files_excluded: Lists what was left out of the upload, source maps included.root-stripped: Names theindex.htmlthe CLI used when it was not at the top of the folder.cover_included: Names a cover added from outsidebuild.output.engine-default: Your engine ships an address as a default, such as Phaser’shttps://phaser.io/, and calls it only if you use that feature. Any other address on that host is listed in theexternal-hostsnote like any other outside host.raw-runtime-shell: The build has noindex.htmland one complete SWF or.jsdosfile, so OpenGame will make a page for it. The server still checks the file, and a reviewer still plays the game.platform-engine: Names the engine the check found. See what we accept.platform-account: Players can play without an OpenGame account. Use the SDK if you want account-backed scores or sign-in, and try them in your preview.
root-chosen, naming the folder that was picked, and source-maps-dropped, counting the source maps left out. Engine notices such as engine-modal-dialog, and the runtime and vendoring notices runtime_acquisition_deferred and dependencies_vendored, are in the export workflow.
Where each check runs
The CLI’s checks save you a round trip. The server repeats them after upload.