Publish a game
A game here is a folder: your HTML, your scripts, your art, and an index.html that starts the thing. If it runs when you open that file on your own machine, it runs here. Hearth exports exactly that shape, and so does nearly every web game framework.
Nobody reviews your game, nothing costs money, and players never have to make an account to play it. If you work with a coding agent, the quick setup below is two copy buttons. If you made the game in Hearth, the app publishes it for you and the first of the manual paths is the whole story. If you would rather do it yourself, every step is written out after that.
Quick setup
Give your agent a token and a prompt. It handles the rest.
Make a token
A token lets a script publish as you. Create one here, copy it while it is on screen (that is the only time it is shown), and put it in your shell:
export HEARTH_CATALOG_TOKEN=hpub_your_token_herePaste a prompt to your agent
Run it in the folder your game lives in. Claude Code, Codex, Cursor, or anything else with a terminal will do.
Publish this game
First time out. Your agent reads the game, writes the description and tags, and gives you back a live URL.
Read the prompt
Publish the game in this folder to the Hearth Catalog.
Download https://catalog.hearthengine.com/publish.mjs and run it with Node 18 or newer. It has no dependencies and nothing to install.
Pass --token $HEARTH_CATALOG_TOKEN. Write --title, --tagline, --instructions and --tags yourself based on what the game actually does; read the source if you need to. If the playable build sits in a subfolder like dist or public, point --dir at that folder rather than the repo root.
Print the URL the script gives you at the end. If it exits with an error, the message names the file or field that is wrong. Fix that and run it again.Update a game I already published
Same address, new build. The version people are playing stays up until the new files finish uploading.
Read the prompt
Update the game I already published on the Hearth Catalog so it matches this folder.
Download https://catalog.hearthengine.com/publish.mjs and run it with Node 18 or newer, passing --token $HEARTH_CATALOG_TOKEN and --slug followed by the slug from the game's URL. Reusing the slug updates that page instead of creating a second one, and the live game keeps working until all the new files have uploaded.
Print the URL it gives you.To see it before anyone else does, add a line to either prompt telling the agent to pass --draft. Your game uploads and gets its own page, but stays off the catalog until you publish it from your dashboard.
Doing it yourself
Four ways in, from the one button inside Hearth to the raw API.
Publish the open game from the app
Best if the game is a Hearth project. No terminal, nothing to download.
- Make a token and paste it into Hearth under Settings, then Publishing. It is stored for you rather than for one game, so this part happens once.
- Open the game and press
Publish to the catalogin the top bar. Hearth shows you the entry page it found, how many files would go, and how large they are, before anything is sent. - Write the title, the one-liner, how to play, and tags, pick a cover from a file already in the project, and publish. You get the live address back in the dialog.
- Publishing the same folder again updates that page instead of making a second one, even if you rename the game. The folder remembers which listing is its own.
Hearth is a free, open source app for building browser games with a coding agent. hearthengine.com
Drag the folder onto the page
Best the first time, and whenever you want to edit the text later.
- Open the upload page and drop in your game folder, or a .zip of it. Files that have no business being public, such as
node_modulesand.env, are skipped for you. - Fill in the title, a one-liner, how to play, and tags. The web address is generated from the title, and you can edit it.
- Add a cover image if you have one. It is the first thing anyone sees of your game in the catalog.
- Hit
Publish, or save it as a draft and play it on its own page first. A draft is visible only to you.
One command, no install
Best in a build script, or when the game is already in a repo.
Get the script
One file, no dependencies, Node 18 or newer. Keep it next to your game or anywhere on your path.
curl -fsSL https://catalog.hearthengine.com/publish.mjs -o publish.mjsRun it
With HEARTH_CATALOG_TOKEN in your environment, this is the whole thing. It walks the folder, works out which file starts the game, uploads everything, and prints the URL.
node publish.mjs --token $HEARTH_CATALOG_TOKEN --made-with hearth folder .
title Ember Drift
slug ember-drift
entry index.html
files 1 · 4 KB
publishing as @you
created /g/ember-drift
uploading 1/1
published · 1 files · 4 KB
https://catalog.hearthengine.com/g/ember-driftFill in the details
Everything the upload page asks for has a flag. Pass what you have; skip what you do not.
node publish.mjs \ --dir ./dist \ --title "Ember Drift" \ --tagline "A one-button racer about momentum" \ --instructions "Hold space to drift. Release to straighten." \ --description-file ./about.txt \ --tags "arcade,racing" \ --cover ./cover.png \ --draftRun it again later and the same game is updated rather than duplicated, so it is safe to leave in a deploy script. Keep the slug the same and the URL you shared keeps working. --draft uploads everything but leaves the game unlisted. node publish.mjs --help prints every flag.
Or link to a game that lives somewhere else
A desktop build, a store page, a game already hosted on your own site: list it here and the page sends people there. There is no folder to upload, so you only need the two link flags and whatever text you want on the page.
node publish.mjs \ --title "Ember Drift" \ --external-url "https://your-studio.itch.io/ember-drift" \ --external-kind download \ --tagline "A one-button racer about momentum" \ --cover ./cover.png--external-kind is the word on the button, and it is required: play if the link opens the game in a browser, download if it hands somebody a build to run. Nothing is guessed for you, because a Play button that downloads a zip is a lie.
Hosting the game here and linking somewhere else are not exclusive. Add --dir and both happen: the browser version is playable on the page, and the link sits beside it. On its own, --external-url reads no folder at all.
The API the script is built on
Best when you are writing your own tooling.
Four requests, each authenticated with Authorization: Bearer hpub_…. Replies are JSON. When something fails you get { error: { code, message } } and an HTTP status, and the message is written for whoever has to fix it.
GET/api/v1/me
{ "user": { "id", "username", "display_name" } }Checks the token and tells you which account it publishes as.
POST/api/v1/games
{ "title", "slug"?, "tagline"?, "description"?,
"instructions"?, "tags"?, "made_with"?, "entry_path"?,
"external_url"?, "external_kind"? }{ "created": true, "game": { "id", "slug", "title", "status" } }Keyed by slug. A slug you own already is updated in place. One that belongs to someone else is a 409 if you asked for it by name, or stepped past to -2 or -3 if it came from your title. New games start as drafts. external_url is the https:// address of a game hosted elsewhere and external_kind is play or download; they travel together, external_kind is never guessed for you, and external_url: null clears both.
POST/api/v1/games/{id}/files
{ "files": [{ "path", "size" }, …], "replace"?: true }{ "entry", "replaced", "expires_in": 7200,
"uploads": [{ "path", "url", "token" }, …] }Checks the whole manifest before handing back anything. PUT each file to its url with the Content-Type for its extension and the header x-upsert: true. Those URLs last two hours. Nothing is deleted at this step, so a run that dies halfway leaves the live game exactly as it was; whatever the new build replaced is cleared away by /publish once every declared file has landed.
POST/api/v1/games/{id}/cover
raw image bytes; Content-Type image/png, image/jpeg, image/webp, or image/gif{ "cover": { "path", "url", "bytes" } }Replaces whatever cover was there. Up to 4 MB.
POST/api/v1/games/{id}/publish
{ "entry_path"?, "draft"?: false }{ "url", "game": { "id", "slug", "title", "status",
"entry_path", "file_count", "total_bytes", "published_at",
"external_url", "external_kind" } }Confirms the entry file is really in storage, clears out what the new build replaced, and recounts the size from storage before the game goes live. A game with an external_url and no uploaded files skips that check, since there is no build here to be missing, and goes live as a link. Send draft: true to save the counts and take the game back off the listing.
A run from start to finish
ID=$(curl -s -X POST https://catalog.hearthengine.com/api/v1/games \ -H "Authorization: Bearer $HEARTH_CATALOG_TOKEN" \ -H "Content-Type: application/json" \ -d '{"title":"Ember Drift","made_with":"hearth"}' | jq -r .game.id)# POST /files with your manifest, PUT each url it hands back, then:curl -s -X POST https://catalog.hearthengine.com/api/v1/games/$ID/publish \ -H "Authorization: Bearer $HEARTH_CATALOG_TOKEN" \ -H "Content-Type: application/json" -d '{}' | jq -r .urlLimits and rules
| Files per game | 300 |
|---|---|
| Bytes per file | 32 MB |
| Bytes per build | 200 MB |
| Cover image | 4 MB · png, jpeg, webp, gif |
| Title | 80 characters |
| Tagline | 140 characters |
| Description | 10,000 characters, plain text |
| Instructions | 4,000 characters, plain text |
| Tags | 8 max, lowercase, hyphens allowed |
| Slug | 2 to 64 characters, lowercase, single hyphens |
| External link | https only, up to 400 characters, kind is play or download |
| Tokens per account | 10 |
Your entry file is found for you. Any of index.html, game/index.html, dist/index.html, public/index.html, or a lone .html at the top level, and otherwise you set entry_path yourself. Paths have to be relative and forward-slashed, and anything containing .. is refused, as are node_modules/, .git/, and .hearth/.
A game that goes public has to be something a visitor can press: either files you uploaded here, or an external_url pointing at where it lives. A draft may be neither, since a draft is a work in progress. External links are https only. A catalog that sends readers to an insecure page has made that choice catalog never sends anyone to an insecure page, and every one of them carries a play or download so the button says something true.
Descriptions and instructions are plain text. Blank lines turn into paragraphs and no HTML is rendered. Your game runs in a sandboxed iframe on its own web address, walled off from the catalog and from every other game, and it can use scripts, storage, pointer lock, popups, forms, downloads, and dialogs. The one thing it cannot do is navigate the page it is embedded in.
Tokens are stored as a hash and shown once, when you make them. If you lose one or paste it somewhere it should not be, revoke it from /settings/tokens and it stops working straight away.