Skip to the games
Hearth

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_here

Paste 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.

In Hearth

Publish the open game from the app

Best if the game is a Hearth project. No terminal, nothing to download.

  1. 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.
  2. Open the game and press Publish to the catalog in 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.
  3. 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.
  4. 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

In the browser

Drag the folder onto the page

Best the first time, and whenever you want to edit the text later.

  1. 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_modules and .env, are skipped for you.
  2. 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.
  3. Add a cover image if you have one. It is the first thing anyone sees of your game in the catalog.
  4. Hit Publish, or save it as a draft and play it on its own page first. A draft is visible only to you.
In the terminal

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.

your-game
curl -fsSL https://catalog.hearthengine.com/publish.mjs -o publish.mjs

Run 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.

your-game
node publish.mjs --token $HEARTH_CATALOG_TOKEN --made-with hearth
output
  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-drift

Fill in the details

Everything the upload page asks for has a flag. Pass what you have; skip what you do not.

your-game
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 \  --draft

Run 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.

Over HTTP

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

reply
{ "user": { "id", "username", "display_name" } }

Checks the token and tells you which account it publishes as.

POST/api/v1/games

body
{ "title", "slug"?, "tagline"?, "description"?,
  "instructions"?, "tags"?, "made_with"?, "entry_path"?,
  "external_url"?, "external_kind"? }
reply
{ "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

body
{ "files": [{ "path", "size" },], "replace"?: true }
reply
{ "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

body
raw image bytes; Content-Type image/png, image/jpeg, image/webp, or image/gif
reply
{ "cover": { "path", "url", "bytes" } }

Replaces whatever cover was there. Up to 4 MB.

POST/api/v1/games/{id}/publish

body
{ "entry_path"?, "draft"?: false }
reply
{ "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

your-game
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 .url

Limits and rules

Files per game300
Bytes per file32 MB
Bytes per build200 MB
Cover image4 MB · png, jpeg, webp, gif
Title80 characters
Tagline140 characters
Description10,000 characters, plain text
Instructions4,000 characters, plain text
Tags8 max, lowercase, hyphens allowed
Slug2 to 64 characters, lowercase, single hyphens
External linkhttps only, up to 400 characters, kind is play or download
Tokens per account10

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.

Publish a game · Hearth Catalog