Boxity SDK
One drop-in SDK for cross-game identity. Add Boxity login, avatars,
friends & presence, synced settings and Gems in-game currency to any
HTML5 or Unity WebGL game โ embedded on
bloxity.io or standalone on your own domain.
The SDK is published globally as window.Legion.SDK (the platform was formerly
called Legion โ the package and namespace keep that name; the product is
Boxity). It works two ways automatically:
- Embedded โ your game runs in an iframe at
bloxity.io/g/<your-slug>. Auth, friends and purchases flow through the portal overpostMessage(no extra HTTP). - Standalone โ your game runs on your own domain. The same calls fall back
to the Boxity REST API (
apiUrlof the told environment) and a login popup window.
You write the same code for both โ the SDK detects its environment and routes accordingly.
โธ Standalone (own domain): bloxity.io/test-game.html
โธ Embedded (in the portal): bloxity.io/g/test-game
Open both side by side to see how the same code behaves embedded vs standalone.
How it works #
| Capability | Embedded (iframe on bloxity.io) | Standalone (your domain) |
|---|---|---|
| Login | In-game modal overlay | Popup window โ bloxity.io/auth |
| Friends / invites | postMessage to portal | GET /v1/social/connections |
| Settings | Pushed live from portal menu | Pushed from portal when present; defaults otherwise |
| Gems purchase | Portal renders the confirm modal | SDK renders the modal โ POST /v1/bux/purchase |
| Avatar | From the portal handshake | GET /v1/avatar/equipped |
https://bloxity.io ยท
API https://api.bloxity.io ยท SDK https://sdk.bloxity.io ยท
CDN https://static.bloxity.io. Stage and local use other hosts, and the platform tells
your game which ones apply. See Environments.What happens when your game boots (the handshake)
Almost every "why doesn't the SDK work?" question comes down to timing โ calling
something before the data it needs has arrived. Here is the exact boot sequence when your game is
embedded on bloxity.io, so you know precisely when each piece of data is
ready:
- The portal opens your game in an iframe t = 0
A loading overlay is already covering your game. The portal is waiting to hear from your code.
- The SDK script loads <script> onload
window.Legion.SDKnow exists. Nothing before this point can touch the SDK, so put your integration code after the script tag (or in itsonload). - You call
Legion.SDK.init()you, onceThe SDK immediately posts a handshake request and an auth request to the portal, then returns.
init()is synchronous and returns before the portal answers โ at this instantgetUser()is stillnull. - Your
onUserChanged/settings.listencallbacks fire once immediately microtaskThey fire right away with the current (empty) state:
onUserChangedwithnull, eachsettings.listenwith its default value. This is why you subscribe first and let the callback drive everything โ it will fire again with real data. - The portal answers with the handshake ~1 round-trip
One reply carries everything at once: the logged-in user (or
nullfor a guest), the access token, the equipped avatar, the resolved gameSlug, the debug flag, and the current settings. The SDK stores it all and fires your callbacks again โonUserChanged(now with the real user),avatar.onAvatarChanged, and everysettings.listenโ with the real values. - Your game finishes its own loading, you call
loadingEnd()you, when readyThe overlay lifts and the player is in. (See Game lifecycle for the loading-screen rules.)
getUser() right after init() and assume it's
populated โ it isn't yet. Drive your UI from onUserChanged, which fires again when the user
arrives. (2) There is no timeout on the handshake; if the portal is slow,
your callbacks simply fire later. Subscribe before you need the data, not after.
(3) Register your settings.listen() keys and your onUserChanged
subscription immediately after init(), before loadingEnd(), so the
first push from the portal isn't missed.Standalone (your own domain) is simpler: init() reads a saved token from
localStorage, drops it if expired, and calls GET /v1/auth/me to refresh the user.
No iframe, no postMessage, no portal. onUserChanged fires with null, then again
with the restored user once that request returns (or stays null if there's no valid token).
Install & URLs #
The SDK is a single script โ no build step, no npm install required for the browser global.
<!-- Production (minified) -->
<script src="https://sdk.bloxity.io/legion-sdk.min.js"></script>
<!-- Pinned v1 (recommended for production stability) -->
<script src="https://sdk.bloxity.io/v1/legion-sdk.min.js"></script>
<!-- Development (unminified + source map, verbose logging) -->
<script src="https://sdk.bloxity.io/legion-sdk.js"></script>Access-Control-Allow-Origin: *
and Cache-Control: no-cache (ETag revalidated) so games never run a stale build after a deploy.Quick start #
The smallest working integration โ log a user in and react to login state:
<script src="https://sdk.bloxity.io/legion-sdk.min.js"></script>
<script>
// 1. Initialize once. gameSlug is the slug your game is registered under
// on bloxity.io (required for Gems purchases; auto-filled when embedded).
Legion.SDK.init({ gameSlug: 'my-game' });
// 2. React to login state. Fires immediately with the current user
// (or null), then again on every login / logout.
Legion.SDK.auth.onUserChanged((user) => {
if (user) {
console.log('Hello', user.displayName || user.username);
} else {
console.log('Not logged in');
}
});
// 3. Open the login UI (auto-picks modal vs popup) from a button.
document.getElementById('login').onclick = () => Legion.SDK.auth.showAuthPopup();
</script>Environments: prod, stage, local #
Boxity runs as three environments. The same game build runs in all of them, and the platform tells the game which one it is in. Your game never works it out for itself.
| prod | stage | local | |
|---|---|---|---|
| Portal | https://bloxity.io | https://stage.bloxity.io | http://localhost:3000 |
| API | https://api.bloxity.io | https://api.stage.bloxity.io | http://localhost:4000 |
| Matchmaker | https://play.bloxity.io | https://play.stage.bloxity.io | your local server |
| Asset CDN | https://static.bloxity.io | https://static.stage.bloxity.io | https://static.bloxity.io |
| Game page | https://<id>.play.bloxity.io | https://<id>.play.stage.bloxity.io | http://localhost:<port> |
| Game server | https://<id>.host.bloxity.io | https://<id>.host.stage.bloxity.io | http://localhost:2567 |
location.hostname, the referrer, a build flag or any URL, and never write
api.bloxity.io, play.bloxity.io or static.bloxity.io into code,
build config or .env files. Read the told environment, and fall back to prod only when
nothing was told. That fallback is what keeps existing games working unchanged.In the browser
Boxity tells a page its environment in two ways. Pages it serves get a
window.BLOXITY_ENV object injected before your code runs. Games embedded in a Boxity
page also receive it in the portal handshake. The SDK reads both, so after the script loads, ask the
SDK:
const { env, apiUrl, playUrl, portalUrl, staticUrl } = Legion.SDK.environment();
// env: 'prod' | 'stage' | 'local'. Production when nothing was told.
// Embedded games learn it from the handshake, which can land after init:
Legion.SDK.onEnvironmentChanged((e) => reconnectTo(e.playUrl));The SDK uses the same values for its own calls: login, avatar, Gems, Legion.SDK.api,
Legion.SDK.net and party tickets. Code that needs a URL before the SDK has loaded reads
window.BLOXITY_ENV through one helper with the prod fallback:
// boxityEnv.js - the only place Boxity URLs come from
const PROD = {
env: 'prod',
apiUrl: 'https://api.bloxity.io',
playUrl: 'https://play.bloxity.io',
portalUrl: 'https://bloxity.io',
staticUrl: 'https://static.bloxity.io',
};
export function boxityEnv() {
if (window.Legion?.SDK?.environment) return window.Legion.SDK.environment();
const told = window.BLOXITY_ENV;
return told && told.apiUrl ? told : PROD;
}
// e.g. fetch(`${boxityEnv().apiUrl}/v1/avatar/emotes`)On your game server
Boxity hosting injects these variables into every deploy of your server:
export const boxity = {
env: process.env.BLOXITY_ENV || 'prod',
apiUrl: process.env.BLOXITY_API_URL || 'https://api.bloxity.io',
matchmakerUrl: process.env.BLOXITY_MATCHMAKER_URL || 'https://play.bloxity.io',
portalUrl: process.env.BLOXITY_PORTAL_URL || 'https://bloxity.io',
staticUrl: process.env.BLOXITY_STATIC_URL || 'https://static.bloxity.io',
};
// e.g. POST `${boxity.apiUrl}/v1/auth/game-token/verify`Local development
- Inside the local portal (
http://localhost:3000/g/<slug>): nothing to do. The portal sendsenv: 'local'in the handshake. - Standalone (your dev server on its own): tell the page itself, before the game's
code runs. One line in
index.html:
<script>window.BLOXITY_ENV = { env: 'local', apiUrl: 'http://localhost:4000', playUrl: 'http://localhost:2567', portalUrl: 'http://localhost:3000', staticUrl: 'https://static.bloxity.io' };</script>With Vite, keep that tag out of production builds by injecting it only from the dev server:
// vite.config.js
const LOCAL_ENV = { env: 'local', apiUrl: 'http://localhost:4000', playUrl: 'http://localhost:2567', portalUrl: 'http://localhost:3000', staticUrl: 'https://static.bloxity.io' };
export default {
plugins: [{
name: 'boxity-local-env',
apply: 'serve',
transformIndexHtml: () => [{ tag: 'script', children: `window.BLOXITY_ENV = ${JSON.stringify(LOCAL_ENV)}`, injectTo: 'head-prepend' }],
}],
};Your local server reads the same BLOXITY_* variables from its own .env, e.g.
BLOXITY_ENV=local, BLOXITY_API_URL=http://localhost:4000.
Release flow: one build, both platforms
No extra branch. Your branches mean what they always did, and every push goes to both platforms:
main→ deploys the same build to prod and, when your repo has a stage token, to stage as well. The two deploys are independent: a staging failure never blocks or undoes prod.dev→ your playtest channel on prod (<game>.dev.play.bloxity.io) and, with a stage token, on stage (<game>.dev.play.stage.bloxity.io).- The Run workflow button deploys any branch to the platform you pick, e.g. to try a change on stage before it reaches players.
Staging is opt-in. Each environment has its own hosting API and its own deploy token: create the prod
token on hosting.bloxity.io (secret LEGION_DEPLOY_TOKEN) and, if you want your
game on stage, a stage token on hosting.stage.bloxity.io (secret
LEGION_DEPLOY_TOKEN_STAGE). A token from one environment never works on the other. Each
environment lists only the games deployed to it.
# .github/workflows/deploy.yml
on:
push:
branches: [main, dev]
workflow_dispatch:
inputs:
target: { type: choice, options: [staging, prod], default: staging }
jobs:
targets:
runs-on: ubuntu-latest
outputs: { list: ${{ steps.t.outputs.list }} }
steps:
- id: t
env: { HAS_STAGE_TOKEN: ${{ secrets.LEGION_DEPLOY_TOKEN_STAGE != '' }} }
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo 'list=["${{ inputs.target }}"]' >> "$GITHUB_OUTPUT"
elif [ "$HAS_STAGE_TOKEN" != "true" ]; then echo 'list=["prod"]' >> "$GITHUB_OUTPUT"
else echo 'list=["prod","staging"]' >> "$GITHUB_OUTPUT"; fi
deploy:
needs: targets
runs-on: ubuntu-latest
strategy:
fail-fast: false # staging can never take prod down with it
matrix: { target: ${{ fromJSON(needs.targets.outputs.list) }} }
env:
BLOXITY_API_URL: ${{ matrix.target == 'staging' && 'https://api.stage.bloxity.io' || 'https://api.bloxity.io' }}
BLOXITY_DEPLOY_TOKEN: ${{ secrets[matrix.target == 'staging' && 'LEGION_DEPLOY_TOKEN_STAGE' || 'LEGION_DEPLOY_TOKEN'] }}
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build # one build: no environment baked in
- run: npm run deploy # your deploy script reads BLOXITY_API_URL + BLOXITY_DEPLOY_TOKENGetting your game onto stage
Stage is a separate platform with its own accounts, hosting whitelist and catalog, so a game joins it once:
- Create an account on
https://stage.bloxity.io(your prod account does not exist there) and ask the Boxity team to enable hosting for it on stage. - On
https://hosting.stage.bloxity.io, create your game with the same game id it has on prod. Copy the deploy token it shows. - Add it to your repo as the secret
LEGION_DEPLOY_TOKEN_STAGE. KeepLEGION_DEPLOY_TOKENfor prod; never swap them. - Push. With the workflow above, the same build now ships to both platforms. Your game is playable at
https://<game>.play.stage.bloxity.ioright away. - Ask the Boxity team to list it on stage. Each environment lists only the games deployed to it, and a
game appears on
stage.bloxity.ioonce its card is added there.
Build with the Boxity MCP #
Connect the Boxity MCP server to your coding agent (Claude Code, Codex, Cursor, Gemini CLI)
and build Boxity games from it. Nothing to install: the server is hosted at
https://mcp.bloxity.io/mcp. The live preview runs on Boxity โ you never run the game locally.
Connect
# Claude Code โ then run /mcp, pick boxity, choose Authenticate, sign in and click Allow
claude mcp add --transport http --scope user boxity https://mcp.bloxity.io/mcpOther agents: add the same URL as a remote (HTTP) MCP server and sign in when asked. Agents without
OAuth can use a personal token from bloxity.io/create as
Authorization: Bearer <token>. Connected agents are listed (and can be disconnected)
in Settings โ Connected agents.
The loop
- Start: ask your agent to
create_project(a new game from the SDK starter, with every SDK feature already wired) orclone_game(one of your games, or one marked remixable). - Code: the agent
git clones the returned remote and edits locally. - Preview:
git push origin HEAD:devrebuilds the live preview in seconds atbloxity.io/create/<slug>โ inside the real Boxity player, with your avatar. Only thedevbranch can be pushed; game-server changes take a few minutes. - Share:
get_share_linkgives a link anyone can play before publishing; rotate it any time. - Check:
audit_sdkchecks every SDK feature that fits the game (required to publish);integrate_sdkreturns exact, project-specific fixes for anything missing. - Publish:
publish_game. Accounts with creator access publish at once; everyone else is reviewed by the Boxity team first.
BLOXITY_CODE_METADATA.md under
notApplicable: with a reason. Reviewers see the reason.BLOXITY_CODE_METADATA.md with canDie: true (the game then
needs the death screen) or canDie: false. A fall that just resets the
player counts only if you want it to.โก Integrate in one AI prompt
Copy a prompt below and paste it into your AI coding agent (Claude Code, Cursor, etc.) inside your game's repo. Each prompt is fully self-contained โ it tells the agent the entire SDK surface needed to wire up Boxity end-to-end.