TPG Game Authoring Guide
Start with the Game Capability Matrix to confirm that the game category, participant count, message rate, payloads, and persistence model fit the supported platform envelope.
The same matrix publishes the reviewed browser-frame capability mapping. Declare optional browser features there instead of depending on ad hoc iframe exceptions.
This guide covers the current author workflow end to end:
- build an iframe-hosted game with
@tpgames/game-kit - test it locally with
@tpgames/sdk-dev-kit - package the game as a
.zip - upload, submit, and publish it through the registry when it is ready
Important current-state note:
- the SDK, creator upload flow, reviewer flow, published manifest path, and published asset hosting are real today
- uploaded bundles are now discoverable and playable through the room shell once they are published
- public author/runtime packages are published under
@tpgames/*on npm and install without a package token or repository access - new browser game examples should prefer
@tpgames/game-kitfor iframe surfaces; it re-exports the public SDK/runtime types and owns the default postMessage bridge bootstrap so authors can choose React, Three.js, p5, Phaser, or another renderer separately npm create @tpgames/game@latestis the supported zero-monorepo path for creating a renderer-free TypeScript starter; the checked-in template and external-repo fixture remain the contract and detached proof harnessestpgames init <target-dir>creates the same starter when@tpgames/sdk-dev-kitis already installed; generated projects include standalone AI-agent guidance for TP Games development
Prerequisites
Third-party game authors need Node.js 24, npm, and a browser. Install
cloudflared when you want to test a local game with physical devices. You do
not need to clone this monorepo, configure GitHub Packages, or provide an npm
token.
The public package map is:
Start a standalone project from any directory:
With @tpgames/sdk-dev-kit already installed, the equivalent shorter command is
tpgames init my-party-game.
Only contributors working inside the TPG monorepo need its Bun dependencies:
Useful local commands:
For the author workflow, dev:web and dev:registry are the important two.
1. Create an iframe game with @tpgames/game-kit
Install the renderer-neutral game kit in an existing game package:
Bun users can run bun add @tpgames/game-kit and
bun add --dev @tpgames/sdk-dev-kit @tpgames/core-manifest instead.
@tpgames/game-kit re-exports the public SDK helpers and owns the normal iframe
runtime bootstrap. Bring React, Three.js, p5, Phaser, Pixi, Kaplay, or another
renderer separately.
Start with a small typed game definition:
If you are building a reusable non-iframe game package or an advanced bridge
integration, import the lower-level game definition APIs from @tpgames/sdk
directly and pair them with the appropriate runtime or bridge package.
Core SDK helpers already in the public surface:
defineGame()for the low-level lifecycle APIdefineSimpleGame<T>()for typed shared-state authoringapi.reportAnalytics()for safe creator-facing game milestonescreateDeadline()for round timers and voting windowssetPhase()for explicit phase transitionssyncPlayerValues()for controller-aligned collections
The authoritative runtime contract lives in the Game SDK Guide. In particular, each iframe runs its own game definition, readiness hooks can repeat, and outbound state setters are reflected by a later shell echo. Follow the lifecycle/readiness model and state/message decision guide when choosing a hook or channel.
Treat controller identity and authority as live runtime data rather than bootstrap constants:
api.me() is the canonical participant record after the live roster arrives;
context().participantId is the fallback during early surface boot. subscribeContext immediately
replays the current context, then reports effective authority, room, presentation, and participant
changes after the context() getter has been updated. Keep its returned unsubscribe function for
subscriptions that should end before the iframe is removed.
2. Test Locally Before Packaging
In-memory harness
@tpgames/sdk-dev-kit gives you a fast author loop without the full shell:
Use this for:
- lifecycle transitions
- participant joins/leaves
- shared state changes
- transient runtime messages
- analytics milestones emitted through
api.reportAnalytics()
Multi-surface browser workbench
Game repositories created from the starter run the public workbench with the normal development command:
The Vite plugin in @tpgames/sdk-dev-kit/vite opens a host display,
two configurable controllers, and an optional spectator at
/__tpg/workbench. Use it to drive shell-owned lifecycle and settings, toggle
readiness, simulate joins/leaves/reconnects and authority transfer, inspect
shared/player state and runtime messages, and reproduce latency, jitter, loss,
and reconnect scenarios with a deterministic seed. Each embedded surface can
be zoomed independently or opened in a synchronized window.
The workbench consumes only public game-kit and bridge contracts, so the same
setup works in a standalone game repository. Its published-room preset uses
the production hosted-frame sandbox (allow-scripts allow-same-origin) and
canonical participant roles. Native form submission is intentionally unavailable;
handle buttons and Enter explicitly through script events. For transport negotiation and
device-specific input, run the same source in a real room without uploading:
The command starts Vite on loopback, creates a temporary Cloudflare Quick
Tunnel, and opens a host link on https://play.tp.games. The room QR and
controller invite appear after the shell creates the room. Scan the QR on as
many devices as the game needs. No registry upload, API key, or bundle build is
involved.
A newly selected game instance receives absent shared/player values at revision
0. The shell replays the current per-state revision before lifecycle ready,
increments only the state that changes, and clears those values and revisions
when another game is selected. Reconnects within the same game instance retain
the current state.
To use a tunnel you manage, run
cloudflared tunnel --url http://127.0.0.1:5173, then run
npm run dev:devices -- --port 5173 --public-url <https-origin>. For a named
tunnel, configure that hostname to forward to the same loopback port and start
it with cloudflared tunnel run <name>. The CLI does not stop user-managed
tunnels; --no-open prints the host link instead of opening it.
Each room's development selection expires eight hours after launch and uses the
unreviewed iframe capability policy. While npm run dev:devices and its tunnel
remain running, the generated link can launch another room with a fresh
eight-hour selection. A Quick Tunnel exposes the local Vite server and source
modules publicly. Treat the generated host link as private because it contains
the temporary development-manifest descriptor; share only the room QR or
controller invite. Keep .env files, credentials, and private assets outside
Vite's served root, and stop the command and tunnel when testing is finished.
Final asset paths and reviewed browser permissions still need packaged-bundle
verification before publishing.
Accessibility evidence required for publication
Registry review covers every declared host-display, controller, and spectator surface. Before submission, render each surface deterministically and record an automated audit with no serious or critical findings or render failures. Each surface needs a page title or primary heading, named controls, visible focus, bounded live feedback, non-color cues, and reduced-motion behavior where motion is present.
Install Chromium for the published manifest CLI once, then run the generic audit against the built
bundle directory. The starter includes the audit:accessibility script:
The command loads every manifest surface in Chromium, runs axe, writes one source-linked result per
surface, and exits non-zero for render failures, serious or critical findings, missing keyboard
evidence, or missing non-pointer evidence for canvas, drag, or role="application" gameplay. Upload
the generated JSON in the reviewer workflow; the registry validates that it covers the exact
manifest surface IDs, kinds, and entries before approval. The audit server resolves every asset to
its real path inside the built root and blocks game-originated external HTTP and WebSocket traffic,
so deterministic audits cannot read through bundle symlinks or call third-party services.
Reviewers exercise every core gameplay action with a keyboard. Canvas, pointer, drag, and gesture mechanics must provide a verified non-pointer path. A manual result is appropriate only when automation cannot exercise the behavior; its review evidence must identify the reviewer, affected surface and action, result date, and a concrete expiry or removal condition. Findings in a standalone game belong in that game's repository. The registry's release certification blocks approval until the surface audit, keyboard gameplay, non-pointer path, and reduced-motion checks are all confirmed.
To share a display session with a game already selected, link to the public home route with the published game ID:
The value must use the manifest gameId format (a-z, 0-9, and hyphens).
The shell ignores malformed values and uses the normal default selection.
Published-game links do not accept a manifest URL; external games must first
pass through registry review and publication, after which their published
gameId works with the same link. The dev:devices command separately creates
a short-lived development link with the required manifest validation fields.
3. Package the Game Bundle
The registry upload contract is a single .zip archive. At minimum it must contain:
- a root
manifest.json - every required surface asset referenced by the manifest
Typical bundle shape:
spectator.html is optional if the surface is optional.
Build browser surface HTML through Vite rather than copying source HTML after the build. Make each
manifest entry an HTML input, set base: "./", and import shared CSS from every surface module that
uses it. Vite writes the extracted stylesheet and injects its final hashed link into the generated
HTML. Then let tpgames bundle add the manifest and archive without overwriting those files. Strict
validation rejects broken HTML asset links and bundles that contain extracted CSS but leave a
declared surface unable to reach a bundled stylesheet.
Manifest example
Rules that matter today:
manifest.jsonmust be at the archive root- every required
entrymust exist in the archive sdkVersionshould match the current public SDK version- published manifests get an
entryBaseUrl, so manifest entries should stay stable and bundle-relative, for example/host.html displayInteractiondeclares whether the shared host display ispassiveorinteractive; usepassivefor controller-led party games, and move progression, lobby, settings, and other actions to the organizer controllergameMetadatais optional for legacy bundles but recommended for new uploads; it gives discovery, analytics, and future payout reports stable category, session-pace, interaction-mode, and player-count segmentspresentationMetadatais optional but recommended for new uploads; registry discovery combines it withgameMetadataandsessionTopologyintoselectionMetadataso controller game cards can render without loading the full bundleportableDisplayis optional; declare it only when the game emits a strict portable scene, list every primitive required for correct play, and followportable-display-renderer.md- selection-card artwork should be square and readable at small sizes; provide
cardat 512x512 or larger when possible, andthumbnailat 256x256 or larger for compact surfaces - allowed
primaryCategoryandsecondaryCategoriesvalues aretrivia,word,drawing,social-deduction,party-activity,strategy,puzzle,arcade,conversation, andutility playerCount.minmust be positive,maxmust be at leastmin, andrecommendedmust fall inside the declared rangepresentationMetadata.cardTitleis optional short overlay text for square controller cards; it is normalized to uppercase and should stay readable over artworkpresentationMetadata.legacyCategoriesis optional and can include2d,word-games, andrealtimeto place a game into the phone catalog tabs inherited from the legacy productpresentationMetadata.tagsmust be lowercase slugs such aspartyorspeed-roundpresentationMetadata.launchColoroptionally chooses the six-digit hex color used to cover the lobby while this game launches; games without one fade through whitepresentationMetadata.artworkvalues must behttp/httpsURLs or safe bundle-relative image paths ending inavif,gif,jpg,jpeg,png,svg, orwebp; parent-directory traversal and script URLs are rejected, and published bundle paths are served back through the registry's/published-assets/...origin in discovery
Create the archive however you prefer. Example:
The public tpgames bundle command keeps the flow independent of host CLI
tools and system zip binaries. Each detached project describes its files in
tpgames.json.
Strict local preflight
Install @tpgames/core-manifest as a development dependency and run
the same strict contract used by registry ingestion before upload:
Directory validation also checks each .zip in that directory, so a normal
author check can prove both the extracted build and the produced archive.
Diagnostics include the manifest JSON path or bundle file path and reject:
- unknown manifest fields, surface kinds, capabilities, and permissions
- SDK incompatibility and contradictory display/controller/spectator topology
- unsafe archive-relative paths and missing surface or artwork files
- surface HTML without an entry script
- missing bundle-relative scripts, stylesheets, images, audio, or video
- buttons, linked anchors, form inputs, or non-negative
tabindexvalues in a host surface declared as a passive display
The versioned JSON Schema is published at
@tpgames/core-manifest/game-manifest.schema.json; its package file
is schema/game-manifest.v1.schema.json for editor completion. The checked-in
starter wires this preflight into npm run validate and therefore into its
normal npm run check and pull-request workflow.
4. Upload Through the Creator Portal
Start the registry and shell locally:
Open:
Deployed creator portal:
The registry worker is the API backend, not the UI host. If someone lands on
https://tpg-registry.tp-games.workers.dev/, it should redirect them back to the
web-shell creator portal instead of acting like a product homepage.
Current creator flow:
- choose the
.zip - upload it
- inspect the stored draft record
- submit the draft for review
What the registry validates right now:
- strict manifest schema and SDK compatibility
- recognized capabilities, permissions, and coherent session topology
- required surface and local artwork entries exist in the archive
- surface HTML has an entry script and all bundle-relative HTML assets exist
- bundle metadata is stored with the creator record
- assets stay private until the version is published
5. Upload Through the API
The portal is just a UI over the registry API, so CI or build-pipeline uploads can hit the same endpoints directly.
For a concrete separate-repo fixture, see:
For framework-specific starting points, use the public examples hub:
For a renderer-free starter, scaffold a customized project from any directory:
The public @tpgames/create-game package embeds the checked-in
templates/game-kit-starter/, sets the package name,
game id, title, and version defaults, and requires a target path that does not
already exist. The checked-in starter contains:
- a minimal bundle template with
manifest.json,host.html, andcontroller.html - a declarative
tpgames.jsoncontract consumed by the public SDK dev-kit CLI without monorepo-only helpers or host-installed packaging CLIs - TypeScript host/controller source that builds with Vite and uses
@tpgames/game-kitfor iframe runtime bootstrap - the canonical
tpgames registercommand, which uploads by default and exposes explicit submit and publish modes against the registry API - a GitHub Actions workflow that runs install/typecheck/boundary/build/bundle checks on pull requests, then shells through
npm run register -- --mode publish --yesonly for configuredmainbranch or manual publish runs instead of inlining curl-only logic or bypassing package scripts - shell integration coverage that copies the template into a detached temp directory and runs
npm run typecheck,npm run bundle, andnpm run registerunder a restrictedPATH, so the proof exercises the template's actual package-script contract AGENTS.mdplus a self-containedtpg-game-developmentskill that preserves runtime boundaries and teaches the local, device, validation, and delivery loops without monorepo-only instructions
The starter pins the tested public @tpgames/game-kit version and
declares its runtime contract with sdkVersion in the manifest. Its normal
npm run check command covers typecheck, unit tests, boundary validation,
browser-surface build, and bundle validation. Use npm run publish:dry-run to
inspect the archive and intended registry operation without credentials or a
network mutation, then npm run publish:game when registry credentials are
configured.
For a complete React authoring example, see:
That sample builds React Prompt Relay, a small prompt-and-answer game with:
- Vite-built React host and controller surfaces
- shared prompt state through
api.setSharedState() - player-specific answers through
api.setPlayerState() - lifecycle handling for boot, start, end, settings, and return-to-lobby controls
- a
tpgames.jsoncontract that emits the same rootmanifest.jsonplus surface asset zip expected by the registry - the public
tpgames registercommand and generated GitHub Actions workflow, which use the public registry API instead of private shell internals
For a complete p5.js authoring example, see:
That sample builds p5 Signal Garden, a creative-coding sketch game with:
- a p5 host/display sketch mounted inside the TPG host surface
- DOM-based controller buttons and text input for accessible player actions
- shared prompt state through
api.setSharedState() - player-specific signal state through
api.setPlayerState() - p5-specific setup and teardown guidance for iframe/runtime surfaces
- the same
tpgames.json, public CLI, and generated GitHub Actions workflow shape as the React example
For a complete Phaser authoring example, see:
That sample builds Phaser Rocket Tap, a small arcade-style game with:
- a thin Phaser host scene that renders from a plain external room-state store
- a DOM controller surface for phone input and loading/error states
- shared round state through
api.setSharedState() - player-specific rocket state through
api.setPlayerState() - the same
npm run bundleandnpm run registerdetached repository contract as the React example
For a complete Kaplay authoring example, see:
That sample builds Kaplay Bounce Rally, a small lane-and-bounce game with:
- a Kaplay host display mounted with
kaplay({ global: false, root }) - DOM controller controls for lane movement, bounce input, settings, and lobby return
- shared round state through
api.setSharedState() - player-specific lane and bounce state through
api.setPlayerState() - transient bounce events through
api.broadcast() - the same
npm run bundleandnpm run registerdetached repository contract as the React example
For interactive authoring, sign in through the browser before registering a bundle:
tpgames login displays a short-lived code and opens the TP Games creator
approval page. Opening or signing into that page never approves the CLI by
itself: compare the terminal code and requested scopes, then choose Approve or
Deny. The CLI generates the future credential locally, saves it only after the
registry activates and verifies it, and scopes the saved session to the exact
registry origin. Use tpgames logout to revoke the remote key and remove the
local copy. tpgames logout --local-only is an explicit recovery option when
the registry cannot be reached.
For staging or local registry development, use the same origin for every command:
When running both services locally, configure the registry with
TPG_WEB_SHELL_URL=http://127.0.0.1:3000 (or the actual local shell origin) so
the CLI opens that shell and approval requests are origin-checked correctly.
For a custom HTTPS registry and shell, automatic opening is intentionally
disabled until the author explicitly supplies the exact trusted shell origin:
Without --approval-origin (or TPG_APPROVAL_ORIGIN), the CLI still prints the
validated approval URL so the author can inspect and open it manually. Saved
browser sessions have a 90-day absolute lifetime and can be revoked sooner.
CI remains non-interactive and should use an environment-only creator API key to upload and submit; publication waits for reviewer certification:
Have CI inject TPG_API_KEY from its encrypted secret store; do not type the
secret into an inline command or export it in shell history. Create the key from
the creator portal. Use scopes
games:read, games:write, games:submit, and games:publish for CI jobs
that should upload, submit, and publish a version without reviewer-token
secrets. When both are present, TPG_API_KEY deliberately takes precedence
over a saved interactive session.
Upload:
Add -H "x-tpg-game-visibility: private" to publish without listing the game
in public discovery. The successful response includes a one-time
privateAccessCode; the creator portal turns it into a /private-game#code=…
link. Keep that link secret. Replacing the private link immediately revokes the
old manifest and asset URLs. Use the creator portal to copy or replace links;
the complete code is not stored in readable form and cannot be recovered later.
Submit for review:
Publish from creator-owned CI when the key has games:publish:
Reviewers can still correct structured discovery and analytics metadata before
approval with a reviewer bearer token such as a local demo REVIEWER_TOKEN:
Approve:
Publish:
Fetch the published manifest:
After publish, the manifest response includes entryBaseUrl, and the registry serves the uploaded assets from:
6. Reviewer Flow
Reviewer/admin moderation is no longer exposed on the public web shell.
Use the reviewer API endpoints above for approval and publish operations, or use an internal moderation deployment if your team hosts one.
7. Verification Checklist for Authors
Before uploading a bundle, verify:
- the game logic works in
@tpgames/sdk-dev-kit - local harness code can drive the lifecycle you expect
npm run dev:devicesworks with the required number of physical controllers without a registry upload- every required manifest entry exists in the archive
- the archive root contains
manifest.json - the bundle uploads cleanly through
/creatoror/creator/uploads - the published manifest exposes the expected
entryBaseUrl
8. Common Failure Modes
Upload rejected because an entry is missing
The manifest and archive disagree. If entry is /controller.html, the zip must include controller.html.
Upload rejected because auth is missing
Use either:
- demo bearer tokens in local/dev
- Clerk-backed auth in the deployed creator surface
Published asset returns 404
Assets only become public after:
- upload
- submit
- approve
- publish
Uploaded game does not appear as a playable room option
Published games should appear in shell selection once the registry can serve their published discovery record and manifest successfully.
If a published game is missing from the room shell, check:
- the version is actually
published, not onlyapproved GET /gamesincludes the recordGET /games/<gameId>/manifestresolves anentryBaseUrl- the host and controller HTML assets are reachable under
/published-assets/...
9. Current Next Step
The public npm create @tpgames/game@latest and installed tpgames init
commands initialize the same renderer-free starter without a TPG checkout. The
checked-in starter, packaged CLI proof, detached registry proof, and
public-package availability smoke cover the scaffold, pull request check,
browser-authenticated local delivery, and API-key CI publish contracts.