FORKED.gg Upload a game →
Creator guide

Ship a game on Forked

Everything you need to get a browser game uploaded, reviewed, and live on the Forked node network, with Discord login and durable leaderboards built in. No servers to run, no netcode to write.

What this is

Forked hosts browser games on a decentralized node network. You upload a zip of a game that runs in the browser. We scan it, a human reviews it, and then it goes live at its own link. Two things come free and are the main reason to host here instead of dropping a zip on any static host:

Quick start

  1. Build a browser game with an index.html at the root of the folder.
  2. Add one line to include the SDK: <script src="/forked-sdk.js"></script>.
  3. Zip the folder (the files at the top level, not a folder-inside-a-folder).
  4. Go to the upload page, log in with Discord, and drop the zip.
  5. It shows as pending. A moderator plays it, then approves it.
  6. It goes live at https://vibe.forked.gg/g/<your-game-id>. Share that link.

Packaging your game

The zip

This is the #1 thing that breaks games Host every asset inside your zip. Do not load anything from an external site. For safety, a game runs in a locked sandbox that blocks all outside network calls, so a <script src="https://cdn.somewhere.com/three.js"> or a font pulled from Google Fonts will silently fail and your game will not start. Download those libraries and include them in your zip instead, then reference them with a relative path.

Modern JavaScript is fine

ES modules work. If your game does import * as THREE from './three.module.js', that is supported, as long as three.module.js is in your zip and you load your entry script as a module (<script type="module" src="game.js"></script>). Canvas, WebGL, WebAudio, and gamepad input all work. There is no server-side code and no build step on our end, we serve exactly the files you upload.

Add the Forked SDK

The SDK is one script, served from Forked, that gives your game login and leaderboards. Add it before your own scripts:

<script src="/forked-sdk.js"></script>
<script type="module" src="game.js"></script>

It exposes a global window.forked. Everything is promise-based. If your game is opened outside Forked, the calls simply reject, so guard with a try/catch and your game still runs standalone.

// wait until the bridge is ready, then see who is playing
await forked.ready();
const me = await forked.whoami();   // { player, displayName } or null if not logged in
if (me) console.log("playing as", me.displayName);

Player login

Players sign in with Discord. You do not build any of it. Call forked.login() from a button, and the whole tab goes to Discord and comes back signed in. Check whoami() when your game loads to see if they are already logged in.

document.getElementById("login-btn").onclick = () => forked.login();

// on load
const me = await forked.whoami();
if (me) {
  showHud("Welcome back, " + me.displayName);
} else {
  showLoginButton();   // players can still play logged-out; they just can't post scores
}

Login is optional for playing. It is required only to post a score (so every score has a real person behind it). Reading a leaderboard never needs login.

Scoreboards

This is the part most games want. A scoreboard on Forked is:

Submit a score

// the player must be logged in; boardId is any string you choose
const result = await forked.submitScore("highscore", 4820);
// result: { ok: true, improved: true|false, standing: { rank, score, percentile, ... } }
if (result.ok && result.improved) {
  showToast("New personal best! Rank #" + result.standing.rank);
}

Show the leaderboard

const board = await forked.getLeaderboard("highscore", { limit: 10 });
// board.rows: [ { player, handle, score, rank }, ... ]
for (const row of board.rows) {
  addRow(row.rank, row.handle || "player", row.score);   // show the Discord name
}

// where does the current player sit?
const me = await forked.getStanding("highscore");
// me: { player, handle, score, rank, totalPlayers, percentile } or null
if (me) showYourRank("#" + me.rank + " of " + me.totalPlayers);
Be honest with your players Casual leaderboards are cheat-resistant, not cheat-proof. A score is tied to a real logged-in Discord account and rate-limited, so nobody can post a score as someone else or flood the board. But because the game runs in the player's own browser, a determined player can post an inflated score as themselves. This is the right trade for fun, casual boards. If you need tournament-grade, un-fakeable scores, that needs server-side scoring, which is on the roadmap, not available yet. Say "casual leaderboard" in your UI and you will set the right expectation.

Limits worth knowing

ThingLimit
Boards per game200 distinct boardIds
Submit rate per player, per board~30 per minute (extra submits are dropped)
Score valueany finite number up to 1e15 in magnitude
Rankinghigher wins, ties share the better rank

Events

There is no separate "event" object with a start and stop button today. You run an event using boards, which works well and keeps a permanent history. The key idea: a board is an event.

Starting an event

Pick a boardId that names the event, ideally with a date so it is unique and you keep past events forever:

// during your weekend tournament, submit + read the event's own board
const EVENT = "weekend-cup-2026-07-26";
await forked.submitScore(EVENT, score);
const standings = await forked.getLeaderboard(EVENT, { limit: 20 });

Because boards are durable, last month's "weekend-cup-2026-06-28" board is still readable, so you can show a hall of fame of past winners for free.

Stopping an event

Your game decides when an event is over. When your deadline passes, stop calling submitScore for that board and switch your UI to read-only, showing the final standings with getLeaderboard. A simple client-side check does it:

const EVENT = "weekend-cup-2026-07-26";
const ENDS = Date.parse("2026-07-28T23:59:00Z");
const live = Date.now() < ENDS;

if (live) await forked.submitScore(EVENT, score);   // event running
const finalBoard = await forked.getLeaderboard(EVENT, { limit: 20 }); // always readable
The honest limit on stopping The "stop" above is enforced by your game, in the browser. The server does not know your event ended, so a determined person could still post a score to that board through the API after your deadline. For casual events that is usually fine (same trade as the leaderboards above). A hard, server-enforced "this board is closed" freeze is not built yet. If you are running something where that matters, tell us and we will add it.

Review and going live

Nothing you upload is public until a human approves it. Here is the whole path:

  1. You upload a zip. It is scanned and stored, and shows as pending in your "your games" list.
  2. A Forked moderator opens it in a sandbox and actually plays it.
  3. They approve it (now live), reject it, or, later, take it down (a takedown is permanent).
  4. Once approved, your game is live at https://vibe.forked.gg/g/<id> and the play link appears next to it in your list.

Approvals are done by hand right now, so give it a little time. Make the moderator's job easy: a game that starts cleanly and does not need external anything sails through.

Updating your game

Fixed a bug, added a level, tuned the difficulty? You can ship a new version without taking your game offline and without losing your scoreboards.

On the upload page, find your game under Your games and hit Update, then pick a new zip. Here is what happens:

  1. Your current version stays live the whole time. Players keep playing it.
  2. The new zip is scanned and stored, and goes into the moderation queue as an update. Your game shows an update in review tag in your list.
  3. A moderator plays the new version in a sandbox (it is not public yet) and approves or rejects it.
  4. On approval, the new version replaces the old one at the exact same link (/g/<id>). Nothing about your URL changes, and your leaderboards carry over untouched. On rejection, your live game is left exactly as it was.

A few rules to know:

Your scoreboards live at the game level, not the version level, so a leaderboard set up as an event (a boardId, see Events) keeps every score across an update.

What breaks a game

SymptomCause & fix
Blank screen / nothing happens on PlayYour game loads something from an external URL (a CDN script, a Google font, an image on another host). The sandbox blocks it. Put every file in your zip and use relative paths.
Game not playable after approvalindex.html is not at the root of the zip. Re-zip so index.html is at the top level.
Scores do nothingThe player is not logged in. submitScore needs login. Call forked.login() first.
Leaderboard shows discord:12345 style idsShow row.handle, not row.player. player is the stable id; handle is the display name.
Save data shared between gamesGames share browser storage on the play domain, so do not keep secrets in localStorage. Per-game isolated storage is a planned improvement.

SDK reference

CallReturnsNotes
forked.ready(){ framed: true }Resolves when the bridge is up.
forked.whoami(){ player, displayName } or nullWho is logged in. null if not.
forked.login()navigates the tabSends the player to Discord, then back. Re-check whoami() on load.
forked.submitScore(boardId, score){ ok, improved, standing } or { error }Needs login. improved is true only on a new best.
forked.getLeaderboard(boardId, {limit}){ rows: [{ player, handle, score, rank }] }Public. Default limit 10, max 1000.
forked.getStanding(boardId){ player, handle, score, rank, totalPlayers, percentile } or nullThe logged-in player's own position.

Questions, or want a server-enforced event freeze or tournament-grade scoring? Reach out and we will help. Ready to ship? Upload your game →