Skip to the content
The room

Documentation · Hosting

Hosting

Your web game plays in Player Two’s player either way. Hosted here, it gets its own address on playertwo.cc and needs no code changes. On your own site, your hosting stays yours, and Connect brings the rest. Back to the documentation.

  1. You push its folderOne command, or a zip. Only the files that changed go up.
  2. It gets its own addressyour-game.playertwo.cc, with its build at the top and the SDK put in its page for you.
  3. Players play it hereIn Player Two's player. Its saves follow them to any device; a paid game opens only for its owners.
On this page

The two ways

You choose when you submit the game, and can switch in your studio (its card, under Where it plays). Either way players find it on its page here and play it in Player Two’s player.

What each way gives
On Player Two (recommended)On your own site
How it gets hereUpload a zip, or push the folder with one command. Up to 5 GB.Paste its address.
Its addressIts own, on playertwo.ccYours
Code changesNone. Player Two puts the SDK in its page.One script tag, with Connect on.
SavesWork as in any browser, and follow players to any device.Your site’s, though a frame keeps its own: see Connect.
Selling itOnly its owners can play it. Nothing to build.Your server checks who owns it.
Trophies, leaderboards, friends, invitesBuilt in: a few SDK calls.With Connect, the same calls.
Serving itPlayer Two's, from a CDN.Yours.

Code changes

Hosted here: none

Your export plays as it is. Its paths work as they do on any server, root-absolute ones too (/assets/app.js), and so do its own storage, workers, WebAssembly and audio. Player Two adds one script first in each of its pages, the game SDK in auto mode, which finds Player Two around the game and keeps its saves going:

what Player Two puts in your page's head
<script src="https://playertwo.lol/sdk/v1.js" data-p2-auto data-game="your-game"></script>

If your game already loads the SDK and calls PlayerTwo.init(), nothing changes: it gets the same answer. A query on a file’s address (app.js?v=3) makes no difference to which file is sent.

Your own servers

A game hosted here talks to your servers (an API, a multiplayer server) from its own origin, https://your-game.playertwo.cc. Add it wherever your servers say which origins may call them: your CORS, and your WebSocket server’s check of the Origin header.

allowing the game's address
// your API (Express with cors): the game calls it from its own address on Player Two
app.use(cors({ origin: ["https://yourgame.com", "https://your-game.playertwo.cc"] }));

// your WebSocket server: the same address, in its check of where a connection comes from
const ALLOWED = new Set(["https://yourgame.com", "https://your-game.playertwo.cc"]);
wss.on("connection", (socket, req) => {
  if (!ALLOWED.has(req.headers.origin)) socket.close(1008, "Not from the game");
});

To know who is playing, your servers check a signed identity from p2.identity(), as a game on its own site does: see the identity.

Trophies and leaderboards

A few calls, once you have made the trophies and leaderboards in your studio. The game plays on without them anywhere else.

in your game
PlayerTwo.init().then(async (p2) => {
  await p2.trophies.unlock("first-win");          // the player sees the toast
  await p2.scores.submit("time-attack", 83400);   // a leaderboard you made in your studio
}).catch(() => { /* played outside Player Two: the game carries on without them */ });

Saves

A game hosted here keeps its saves where it always does, in the browser’s localStorage and IndexedDB, at its own address. Player Two keeps a copy of both in each player’s cloud saves and puts it back before the game runs, on any device they sign in on. No code needed.

  • When it goes up: a moment after the game last wrote, and as the player leaves or closes the game. Another device saving in between is answered by sending again on top of it: the newest write wins. At the start of a page load, the cloud’s copy wins.
  • What stays on the device: keys starting p2_ignore or p2-ignore, the keys and databases you list in your studio, databases named for a cache (Unity’s UnityCache), and a database holding files (Blobs), which can’t be copied.
  • How much: up to 2 MB of localStorage and 2 MB of IndexedDB a player. The largest keys and databases are left out until the rest fits, and your studio lists them.
  • Your own saves: p2.saves keeps named slots in the player’s cloud saves, from any game (the SDK). Turn the copy off in your studio if you would rather keep saves that way only.

Unity, Godot and SharedArrayBuffer

Unity

Export for WebGL and push the folder with index.html at its top. Compressed builds work as they are: a file ending .br or .gz is sent with its encoding, so Unity’s decompression fallback isn’t needed. PlayerPrefs and files under Application.persistentDataPath follow the player; Unity’s download cache stays on the device.

Godot

Export for the Web. Files under user:// follow the player. Godot 4 with threads needs SharedArrayBuffer (below); since Godot 4.3, a build exported without threads needs nothing.

SharedArrayBuffer

A page has SharedArrayBuffer (threads in WebAssembly) only when it is cross-origin isolated. Turn Cross-origin isolation on in your studio: your game’s pages then say so in their headers (COOP and COEP, and Document-Isolation-Policy, which isolates the game’s frame on its own). Files your game loads from other sites then need CORS or a Cross-Origin-Resource-Policy header; choose credentialless to let plain ones load, without cookies.

Chrome and Edge isolate the frame today. In other browsers SharedArrayBuffer isn’t there yet inside a frame, so export without threads where your engine can.

Pushing a build

The tool is one file, p2.mjs, with nothing to install. It sends only the files Player Two doesn’t have yet, eight at a time, picks up where it left off if it is stopped, and makes the build live once every file has arrived. Players get the new build on their next page load; a page already open keeps its own to the end.

in a terminal, or your CI
# once: the tool (Node 20 or later), and an agent key from your studio
curl -O https://playertwo.lol/cli/p2.mjs

# then, each time: the folder your engine exported
P2_AGENT_KEY=p2_agent_... node p2.mjs push ./build --game your-game
what it says
Hashing 2,914 files (412.6 MB)...
Build 6f1d0c9a2b7e4f3a8c5d1e0b: sending 38 files (21.4 MB); 2,876 files Player Two has already.
  sent 38 of 38 files, 21.4 MB of 21.4 MB
Live: https://your-game.playertwo.cc/
  • --entry: the page the game opens at, if it isn’t index.html.
  • Left out: .git, .DS_Store and the like, and symbolic links (it says which).
  • The game must be on Player Two first: submit it with a zip, or with its address on your site, then push. The push makes it hosted here.

The three calls

The tool makes three calls, which your own scripts can make too, with your agent key. First the list of files, each with its size and SHA-256; the answer names the build and the files to send, each with where to send it:

1. the list
POST https://playertwo.lol/api/v1/agent/games/your-game/web
Authorization: Bearer p2_agent_...
{ "entry": "index.html",
  "files": [{ "path": "index.html", "size": 1841, "sha256": "9f2c...", "type": "text/html" }, ...] }

200 { "build": "6f1d0c9a2b7e4f3a8c5d1e0b",
      "upload": [{ "sha256": "9f2c...", "method": "PUT", "url": "https://...", "headers": { ... } }, ...] }

Then each of those files, as it is, to its url with its method and headers. The addresses last six hours; ask again with the same list for fresh ones (files already sent aren’t asked for again). Last, the build is checked and made live:

3. done
POST https://playertwo.lol/api/v1/agent/games/your-game/web/6f1d0c9a2b7e4f3a8c5d1e0b/done

200 { "live": true, "url": "https://your-game.playertwo.cc/" }
409 { "code": "missing", "missing": ["path", ...] }       # some never arrived: ask again
422 { "code": "mismatch", "mismatched": ["path", ...] }   # not what the list said: push again

How it's served

  • One build a page load. The page is never cached; each file it asks for comes from that build’s own address (/~<build>/...), cached for a year, since a build’s files never change. A new build is a new address, so no page load mixes two.
  • Played in Player Two’s player only. Only Player Two can frame the game, and a visit to its address goes to its page here, where saves, trophies and friends work. A free game can be played at its own address too: turn that on in your studio.
  • Its files are its own. They are sent to the game’s own pages and to tools, never to another site. Ranges are answered, for audio and video.

Limits

A web build's limits
Up to
A pushed folder5 GB, 20,000 files
One file in it512 MB
A zip (the submit form, or an agent's upload)100 MB; 300 MB and 3,000 files unpacked
One .html page10 MB
A player's copy of localStorage, and of IndexedDB2 MB each

Paths are the folder’s own: none may go outside it. Builds a game no longer plays are removed two days after they are replaced, all but the last two.