Developer Portal Guide
The BodyLink developer portal is the only road to the live catalog. No developer holds catalog write credentials: a bundle goes live exclusively through the portal's fail-closed pipeline —
Developer ──① zip + account login (game ID + version from the manifest)──▶ PORTAL
PORTAL: email/password auth · mint a 128-bit submission ID ("submit code")
· zip → QUARANTINE store (pending/<id>/) · state received → validating
└─② validation dispatched (signed, short-lived URL)──▶ AUTO-VALIDATION (CI)
static gate + headless runtime-verify boot · signed verdict → portal callback
├─ fail ─▶ [rejected: developer reads the named-check report, fixes, resubmits]
└─ pass ─▶ ③ STAGE PUBLISH: bundle → STAGING store · staging catalog rebuilt
· state validated → staged
└─④ reviewer opens the STAGING launcher: REAL launcher UX +
REAL camera, and plays the staged game as human review
├─ reject ─▶ [audit row recorded, developer notified]
└─ approve ─▶ ⑤ PROMOTE staging → production
copy games/<slug>/ staging → PRODUCTION store
· immutable (slug, version) check · audit row
· state staged → published
└─ manifest written last → catalog projector
rebuilds the production catalog (~60s)
└─▶ LIVE in the production catalog
Every release — whether you upload the zip by hand, drive the API directly, or push a GitHub Release — runs this same validate → stage → human-play → promote path. A real person plays your game in the actual launcher before it can reach production. This page walks that pipeline from the developer's side of the screen.
1. Create an account
Registration is email + password self-registration — no third-party sign-in:
- Register with your email and a password of at least 12 characters.
- Passwords are salted and hashed server-side; sessions are cookie-based.
- Your account starts as a submitter. Reviewer rights are granted only by BodyLink to designated reviewer emails — a submitter can never approve anything, including their own game.
You see only your own submissions; reviewers see all.
2. Submit a bundle
Run the preflight first
The game template ships a release preflight that dry-runs every submission requirement before you upload or tag anything. Make it a ritual — run it from your game root before every release:
npm run release:preflight
It prints one line per check and a machine-parseable verdict on the last line
(PREFLIGHT: PASS or PREFLIGHT: FAIL):
✓— the check passed.✗— the check failed; the line names exactly what is wrong, and thefix:line under it is the exact command or edit that repairs it.-— the check was skipped: it doesn't apply to your game yet, or a tool it needs isn't on your machine (the message says which, and how to verify by hand). A skip never fails the verdict; only a real✗does.
The checks, and what each means for you:
| Check | What it asserts | Who it applies to |
|---|---|---|
version-sync |
bodylink.game.json and package.json agree on version, and that version is not already released (release tags are checked locally and on origin). Fix: npm run version:bump -- patch bumps both together. |
everyone |
sdk-sri |
your pinned SDK integrity matches the live SDK channel you target (Stable by default — production requires it; see SDK Channels). Skips if your game isn't cloud-pinned yet. A stale pin ships a bundle that fails SRI on device. | everyone |
bundle-size |
your export zip (an existing one is reused, otherwise it runs the real export:zip) fits your lane's cap — 50 MiB portal upload, 256 MiB GitHub-Release lane (§8). The check detects the lane (a repo with the tag-release workflow is on the GitHub-Release lane; otherwise portal, the strictest cap; --lane overrides), reports which lanes the zip fits either way ("fits portal-upload / fits GitHub-Release lane"), and verdicts against the detected lane. Over 256 MiB fails both lanes. Full cap table: §2 "Size limits & large games". |
everyone |
main-branch |
HEAD equals origin/main — you only release what is merged. Never tag or submit from an unmerged branch. |
everyone |
release-workflow |
your repo's tag-release workflow is present and complete. | GitHub-Release lane only (§8) |
ingest-token |
the release-lane credential is set on your repo. | GitHub-Release lane only (§8) |
The discipline: a ✗ means stop. Apply the printed fix:, re-run, and
proceed only when the last line reads PREFLIGHT: PASS. Never tag a release
from a branch, and never upload anyway "to see what happens" — the portal
enforces the same rules fail-closed, so the failure just resurfaces later
with less context. If a ✗ names something you can't fix yourself, stop and
report it with the preflight output attached instead of working around it.
If you submit through the portal upload (this page, §2–§7), the checks that
must be ✓ before you upload are version-sync, sdk-sri, and
bundle-size. The release-workflow and ingest-token checks belong to the
GitHub-Release lane (§8), which external developers don't use — a ✗ or -
there does not block a portal upload.
Upload
Upload the zip your export step produced (see the Developer Guide §2):
npm run export:zip # -> bodylink-bundles/<id>@<version>.zip
Upload rules, enforced before anything else runs:
- Zip only — the portal checks the content type and the PKZIP magic bytes. A tarball or a renamed directory is rejected on the spot.
- 50 MiB cap on the archive.
- The upload lands in a quarantine store, never the production catalog. Nothing you upload can reach players until validation and a human reviewer both pass it.
Size limits & large games
The size bounds, from the outside in:
| Bound | Limit | Where it's enforced |
|---|---|---|
| Zip archive — portal upload | 50 MiB | at upload; over-cap is a 413, nothing is stored |
| Zip archive — GitHub-Release lane (§8) | 256 MiB, as of the portal's large-game submission update (previously 50 MiB) | at ingest |
| Single asset file | 25 MiB | the validation gate's asset-sizes check |
| Single zip entry | 32 MiB | staging — sits above the 25 MiB validation cap, so no valid bundle hits it |
| Total uncompressed | 512 MiB, as of the large-game submission update (previously 64 MiB) | staging |
| File count | 2000 entries | staging |
What to do when your game is big:
- Shrink the biggest assets first — compress textures, audio, and
models; drop anything unused.
npm run release:preflightreports your exact zip size against the cap. - Keep every single file under 25 MiB — the per-file validation cap applies on every submission lane.
- A game that genuinely cannot fit the 50 MiB upload zip (heavy licensed assets, large audio banks) is a GitHub-Release-lane submission, which accepts release assets up to 256 MiB as of the large-game submission update. That lane is for BodyLink-partnered repos — contact BodyLink if you think your game needs it.
If a bundle that fits these bounds still gets a 413, the portal you are
talking to may not have picked up the large-game submission update yet — the
pre-update bounds (50 MiB archives on every lane, 64 MiB total uncompressed)
still apply there. The 50 MiB portal-upload cap is the same before and after
the update.
A successful upload creates a submission and immediately triggers validation;
the submission moves received → validating.
Submission states
| State | Meaning |
|---|---|
received |
uploaded, validation not yet triggered/running |
validating |
the automated gate is running in CI |
validated |
passed — eligible for staging |
staged |
published to the staging catalog; a reviewer can play it in the real launcher |
rejected |
failed validation, or a reviewer rejected the staged game |
published |
promoted to production; live in the catalog (terminal) |
The staged state is where a human plays your game before it can reach
production (see §4). A staged game is live only in the staging catalog — never
visible to players — until a reviewer approves the promotion.
3. Read the validation report
Validation is the same fail-closed gate you can run locally — it adds no portal-only rules. Because the full gate includes a runtime-verify stage that boots your bundle in a real headless browser (asserting the SDK handshake, tracking frames, and a clean console), and the portal itself has no browser, the portal drives the gate on a CI runner and reads the result back. Non-success of that run — for any reason — is a reject; fail-closed holds end to end.
The report you get is the named-check shape:
{ slug, pass, checks: [ { check, pass, issues: [ { code, message, fix } ] } ] }
Every failing check names its code and a one-line recommended fix, rendered in the portal UI on the submission page. Each code is documented in the Validation Reference — the fastest debug loop is: find the code, apply the fix, re-export, re-run the gate locally, re-upload.
4. Staging — a reviewer plays your game
When your submission passes validation it is not published straight to
production. It is first published to a staging catalog, and its state moves
validated → staged. This is the human gate at the center of the pipeline:
- A BodyLink reviewer opens your staged game in the real launcher — the actual bodylinktv launcher UX, with a real camera — and plays it, exactly as a player eventually will. This is a genuine human review, not just a screenshot or a static check.
- Staging is not visible to players. A
stagedgame lives only in the staging catalog until it is approved and promoted. - Developers cannot self-approve. The reviewer role is server-side and default-deny — a submitter can never approve their own game. (Internal BodyLink reviewers submitting via a GitHub Release are the only exception; see §7.)
- Only a submission in state
stagedcan be approved or rejected — an unvalidated or already-decided submission cannot be pushed through. - Every decision is recorded in an append-only audit trail (who, when, which validation report).
- Very large staged bundles may not render in the portal's inline preview (it keeps a smaller 50 MiB-zip / 64 MiB-uncompressed envelope and degrades to "bundle unavailable" rather than failing). That is expected and does not block review — the reviewer plays the staged game in the real launcher, not the inline preview.
What this means for you: your game has to be presentable and playable from a cold boot, because a real person will pick it up in the launcher and play it. Make sure the first-run experience, the how-to-play, and the tracking all work without any developer setup. If the reviewer can't get into the game, expect a reject with a note.
5. Promote & publish
On approval, the portal promotes your already-validated staged bytes to the
production games store — it copies the exact bundle that was played in staging, so
nothing changes between what the reviewer approved and what ships. Writing the
manifest is the last object written, and it triggers the catalog projector:
the live catalog republishes within about a minute, with no host redeploy.
Your game then appears in consuming host pages. A failed promotion never
half-publishes — the submission stays staged/approved and the promotion is
retried.
Two invariants worth planning around:
- Bundles are immutable per
<slug>@<version>. A published version is never overwritten; publishing into a version that already holds objects is rejected. A new release is the same slug, a higherversion. - The portal never edits the catalog directly — it writes bundles; the projector derives the catalog.
6. Rejects and resubmission
A reject — from validation or from a reviewer — is not a dead end:
- Open the submission in the portal. A validation reject shows the named failing checks with each issue's message and fix; a reviewer reject carries the reviewer's note.
- Fix locally, re-run the gate
(
node scripts/validate-submission.mjs <bundle-dir> --report), and re-export. - Upload again — every upload is a new submission with its own
validation run. If the rejected version never published, you may resubmit
the same
version; if you are superseding a published release, bumpversion.
There is no penalty for iterating: the gate report you see locally is the same one the portal renders, so a resubmission should never be a surprise.
7. The manual submission workflow, step by step
If you are an external developer submitting by hand through the portal UI, this is the whole path end to end:
Scaffold and develop. Start from the game template and build your game locally (
npm run dev, ornpm run dev:hostedfor a real hosted boot — add?camSim=1to drive it without a camera).Build, validate, export. Produce the bundle the portal accepts:
npm run build && npm run validate && npm run export:zip # -> bodylink-bundles/<id>@<version>.zipRunning
validatelocally first means the portal's automated gate should be a formality — it is the same fail-closed gate (§3).Register or log in at the portal with your email and a password of at least 12 characters.
Upload the zip — either through the portal's submit page, or by POSTing it to the submissions endpoint (see §8). The upload lands in quarantine and immediately triggers validation.
Read the report. Watch the submission page. On a reject, read the named-check report (§3), fix locally, re-export, and re-upload — every upload is a fresh submission with its own validation run.
Wait for a reviewer. Once
validated, your game is staged and a BodyLink reviewer plays it in the real launcher (§4). External developers cannot self-approve — a reviewer must approve.Go live. On approval the staged bytes are promoted to production and your game appears in the catalog within about a minute (§5).
8. Headless / programmatic submission (CI)
For a CI job or a programmatic submitter with no browser, the portal exposes the same pipeline as a plain HTTP route chain. Paths below are relative to the portal origin you were issued; the portal sets and expects standard cookies for the session and anti-CSRF double-submit tokens.
GET /auth/csrf→ a pre-auth anti-CSRF token + cookie.POST /auth/registerorPOST /auth/login{ email, password }(echo the token in theX-Portal-CSRFheader) → a session cookie.GET /submissions/csrf(with the session) → the session-bound anti-CSRF token.POST /submissions(the zip + session cookie +X-Portal-CSRF) →{ id, slug, version, state: "validating" }. The portal quarantines the zip and triggers validation on CI.- Poll
GET /submissions/:idforstate— it movesvalidating → validated → staged → published(orrejected), with the validation report attached. - A reviewer plays the
stagedgame in the launcher and calls the approve route; on approval the promotion to production runs and the state becomespublished. GET /submissions/:id/audit(reviewer) returns the append-only decision trail.
Everything the CI chain does is subject to the same validation, staging, and reviewer-approval gates as a manual upload — there is no headless bypass of the human review.
GitHub Release → submission (internal path)
Internal BodyLink games skip the manual upload entirely: cutting a GitHub
Release (vX.Y.Z) on the game repo triggers the portal's github-tag ingest.
The portal maps the repo to a game ID and the tag to a version, pulls the release
zip into quarantine, and re-runs its own authoritative validation gate on the
pulled bytes — the release's own validation report is recorded only as a
cross-check, never a substitute. From there the release follows the identical
path: validate → stage → a reviewer plays it in the launcher → approve → promote →
live. For an internal github-tag submission the reviewer may play and approve
their own release; the external property is unchanged — an external submitter can
never self-approve.
Two release-lane specifics worth knowing:
- Size: as of the portal's large-game submission update, this lane accepts release assets up to 256 MiB (streamed into quarantine — the asset never needs to fit the 50 MiB direct-upload cap). Before that update the lane was bounded at 50 MiB; see §2 "Size limits & large games" for the full table.
- Preflight: on this lane all six
npm run release:preflightchecks apply, includingrelease-workflowandingest-token, and the tag-from-main rule is absolute —PREFLIGHT: PASSbefore you push avX.Y.Ztag, never a tag from an unmerged branch. On a✗you stop, fix or report, and re-run; the release lane asserts the same rules fail-closed at tag time.