Skip docs navigation
The current creator workflow from local SDK testing through bundle upload, review, publish, and verification.

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:

  1. build an iframe-hosted game with @tpgames/game-kit
  2. test it locally with @tpgames/sdk-dev-kit
  3. package the game as a .zip
  4. 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-kit for 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@latest is 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 harnesses
  • tpgames init <target-dir> creates the same starter when @tpgames/sdk-dev-kit is 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 API
  • defineSimpleGame<T>() for typed shared-state authoring
  • api.reportAnalytics() for safe creator-facing game milestones
  • createDeadline() for round timers and voting windows
  • setPhase() for explicit phase transitions
  • syncPlayerValues() 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.json must be at the archive root
  • every required entry must exist in the archive
  • sdkVersion should match the current public SDK version
  • published manifests get an entryBaseUrl, so manifest entries should stay stable and bundle-relative, for example /host.html
  • displayInteraction declares whether the shared host display is passive or interactive; use passive for controller-led party games, and move progression, lobby, settings, and other actions to the organizer controller
  • gameMetadata is 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 segments
  • presentationMetadata is optional but recommended for new uploads; registry discovery combines it with gameMetadata and sessionTopology into selectionMetadata so controller game cards can render without loading the full bundle
  • portableDisplay is optional; declare it only when the game emits a strict portable scene, list every primitive required for correct play, and follow portable-display-renderer.md
  • selection-card artwork should be square and readable at small sizes; provide card at 512x512 or larger when possible, and thumbnail at 256x256 or larger for compact surfaces
  • allowed primaryCategory and secondaryCategories values are trivia, word, drawing, social-deduction, party-activity, strategy, puzzle, arcade, conversation, and utility
  • playerCount.min must be positive, max must be at least min, and recommended must fall inside the declared range
  • presentationMetadata.cardTitle is optional short overlay text for square controller cards; it is normalized to uppercase and should stay readable over artwork
  • presentationMetadata.legacyCategories is optional and can include 2d, word-games, and realtime to place a game into the phone catalog tabs inherited from the legacy product
  • presentationMetadata.tags must be lowercase slugs such as party or speed-round
  • presentationMetadata.launchColor optionally chooses the six-digit hex color used to cover the lobby while this game launches; games without one fade through white
  • presentationMetadata.artwork values must be http/https URLs or safe bundle-relative image paths ending in avif, gif, jpg, jpeg, png, svg, or webp; 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 tabindex values 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:

  1. choose the .zip
  2. upload it
  3. inspect the stored draft record
  4. 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, and controller.html
  • a declarative tpgames.json contract 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-kit for iframe runtime bootstrap
  • the canonical tpgames register command, 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 --yes only for configured main branch 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, and npm run register under a restricted PATH, so the proof exercises the template's actual package-script contract
  • AGENTS.md plus a self-contained tpg-game-development skill 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.json contract that emits the same root manifest.json plus surface asset zip expected by the registry
  • the public tpgames register command 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 bundle and npm run register detached 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 bundle and npm run register detached 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:devices works 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 /creator or /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:

  1. upload
  2. submit
  3. approve
  4. 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:

  1. the version is actually published, not only approved
  2. GET /games includes the record
  3. GET /games/<gameId>/manifest resolves an entryBaseUrl
  4. 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.