The official browser SDK for Prototir, where creators publish playable prototypes and testers play them and leave feedback. A small, typed API for lifecycle signals, analytics events, scores, persistent storage, managed text generation and deterministic random numbers, plus Feedback & tools: Screenshot, Comment, Console and Performance for your testers, with nothing for you to write.
Prototypes run in a sandboxed iframe. The SDK talks only to the Prototir player, over a versioned
postMessage protocol; it holds no credentials and calls no external service of its own.
On Prototir you do not have to: every web build runs with the SDK. If a page does not load it,
Prototir adds it before the page's own scripts, so Feedback & tools work with no setup and
window.Prototir is ready when your code starts. Loading it yourself is harmless and looks like
this, from the player's own path, before your application script:
<script src="/prototir.js"></script>
<script src="app.js"></script>Prototir serves the current SDK from /prototir.js, so a published prototype gets new versions,
including new testing tools, without being uploaded again. Calls such as Prototir.ready(),
event() and score() are optional: they make sessions richer, but nothing breaks without them.
Hosting the build yourself? Use the CDN:
| Address | Behaviour |
|---|---|
https://cdn.prototir.com/sdk/v0/prototir.js |
Follows the newest 0.x release within minutes. |
https://cdn.prototir.com/sdk/v0.3.0/prototir.js |
Pinned and immutable; never changes. |
Each pinned release also has a manifest.json with SHA-384 digests for Subresource Integrity.
npm install @prototir/web-sdkimport { Prototir } from '@prototir/web-sdk';Keep <script src="/prototir.js"></script> in your index.html as well. On Prototir it loads the
current SDK, and the imported one finds it already running and uses it, so your build gets new
testing tools without being rebuilt. Hosted elsewhere, where /prototir.js is absent, the copy
bundled from npm runs instead. Types are included. See the
Babylon.js starter for a
complete Vite setup.
Prototir.ready();
Prototir.event('level_complete', { level: 2 });
Prototir.score(1200);
await Prototir.storage.set('difficulty', 'hard');
const difficulty = await Prototir.storage.get('difficulty');
const quest = await Prototir.ai.generate({
prompt: 'Give the player a short quest hook.',
maxTokens: 80
});Call ready() when the prototype is genuinely interactive, not while it is still showing a loader.
Event names are normalized to lowercase and must contain 1-64 letters, numbers, _, ., : or
-. Keep event payloads small and free of personal data.
| Member | Description |
|---|---|
ready() |
Starts the measured play session when the prototype becomes interactive. |
event(name, data?) |
Records a stable analytics event with an optional JSON-compatible object. |
score(value) |
Reports a finite numeric score. |
storage.get(key) |
Reads a stored string or returns null. |
storage.set(key, value) |
Stores a string scoped to the prototype and signed-in player. |
storage.remove(key) |
Removes a stored value. |
ai.generate(options) |
Generates text with the creator's own key or Prototir AI, as set on the prototype's page. |
rng(seed?) |
Creates a deterministic local random-number function. |
review.enable(options) |
Configures Feedback & tools (on Prototir it is already on; see below). |
review.disable() |
Removes the control and cancels pending requests. |
review.open() |
Opens screenshot feedback from your own UI, for example a pause menu. |
review.capture() |
Takes a screenshot now and opens the composer. |
review.compose(input) |
Opens the composer prefilled with text, context or an image. |
review.comment() |
Opens a comment with no screenshot. |
review.tool(name, on) |
Opens or closes the console or performance panel. |
review.consoleText() |
The console recorded since load, one line per entry. |
review.on(event, handler) |
Subscribes to open, close, submit, error. Returns an unsubscribe. |
Storage keys may contain up to 128 characters, and each value up to 64 KiB of UTF-8. A prototype
never needs service credentials. AI runs either on the creator's own provider key (OpenRouter,
OpenAI, Anthropic, Google and others, saved encrypted in Prototir Studio and never sent to the
browser) or on Prototir AI; routing, moderation and limits are handled by Prototir either way.
Choose it on the prototype's page, or in prototir.json with "ai": { "mode": "own-key" } or
"managed". Pass model to pick one when the prototype's page leaves it empty (the page's choice
always wins): any model your provider offers with your own key ('openai/gpt-5-mini' on
OpenRouter), or a Prototir AI catalog model ('gpt-6-luna'). Handle rejected
requests by their { code, message } value.
Testers get one Feedback & tools control. Its tools unfold inside the same border:
- Screenshot captures the moment; the tester places a pin and writes what they mean.
- Comment is a plain comment on the prototype.
- Console is recorded from load (the last 300 messages and uncaught errors). Testers can copy it or attach it to a comment, where it shows collapsed.
- Performance charts frame rate, slowest frame and memory, and runs only while open. A summary can be copied or attached.
A screenshot, log or summary is always sent with a message: it is what the comment is about, never a comment on its own. Comments follow the prototype's ordinary moderation and comment settings, and turning comments off for a prototype turns feedback off with it.
On Prototir the player draws it: beside Restart and Fullscreen, or inside the Prototir badge in embeds. The SDK stays out of the way and drives the tools inside your frame. Feedback is on by default; the player tells the SDK which prototype it is showing, so there is nothing to call.
Hosted anywhere else, the SDK draws the control itself, bottom-left by default, once you say where comments go:
Prototir.review.enable({
project: 'orbit-garden',
apiBase: 'https://api.prototir.com/api',
slug: 'orbit-garden', // the prototype's slug on Prototir
corner: 'bottom-left', // also bottom-right, top-left, top-right
offset: 16, // pixels from the corner, to clear your own controls
onOpenChange: (open) => setPaused(open)
});The first post asks the tester to approve the build at prototir.com/link; comments then post as
that account. Without apiBase and slug, a self-hosted build shows no feedback control, because
comments would have nowhere to go.
Every tool is on by default. Switch any of them off, or pass false for none:
Prototir.review.enable({ project: 'orbit-garden', tools: { console: false, performance: false } });Console recording starts when the SDK loads, so turning the Console off also stops the recording.
Screenshot and Comment feedback automatically includes browser, OS, viewport, screen, input,
gamepads, session time, recent errors and a frame-rate sample. Set build in review.enable
to include your build label; Prototir also records the build from the play session. Testers can
preview or remove report details before posting. Only the report author and prototype owner
can read them; explicitly attached console logs remain public with the comment.
const stopWatching = Prototir.debug.watch('player.hp', () => player.hp);
Prototir.debug.watch('level', () => currentLevel);
// Later:
stopWatching();
Prototir.debug.unwatch('level');Watch appears in Feedback & tools once a value is registered, including values registered after
the player starts. Readers run only while the Watch panel is open (four times per second), or
when feedback is captured. Each report keeps that moment's values while the tester writes.
Up to 20 names of 1–40 letters, digits, _ . : - are supported, with values shortened to 200
characters. A reader that throws shows its error. Reusing a name replaces its reader.
Use tools: { watch: false } to hide Watch and omit watched values from reports.
review.capture(); // screenshot now, open the composer
review.compose({ // or hand the tester a report already written
text: 'Stuck here.',
context: `gate 3, seed ${seed}`
});
review.on('submit', () => resumeGame()); // also 'open', 'close', 'error'compose accepts an image data URL when your game has a better frame than a live capture: the
frame before a crash, or a rendered diff. Without a capture option the SDK grabs the first
<canvas> on the next animation frame. Supply capture for an exact frame, a WebGL context created
without preserveDrawingBuffer, or a page that is not canvas-based; return a Blob or a
PNG/JPEG/WebP data URL. Screenshots are resized to 1280px on the long edge and sent as JPEG.
Use context: () => ... to record what a screenshot alone cannot: level, seed, elapsed time,
build. A pin on a procedurally generated scene is only reproducible if you write down what
generated it.
The panels follow the player's light/dark preference; pass theme: 'light' or 'dark' to pin it.
Prototir's colours are bundled, so they look right inside a sandboxed frame with no network.
Pointer lock is a browser API and needs no SDK method. Request it from a player action, read mouse deltas only while the canvas is locked, and pause or show a resume action when Escape releases it:
const canvas = document.querySelector('canvas');
canvas.addEventListener('click', () => canvas.requestPointerLock());
document.addEventListener('mousemove', (event) => {
if (document.pointerLockElement !== canvas) return;
rotateCamera(event.movementX, event.movementY);
});
document.addEventListener('pointerlockchange', () => {
setPaused(document.pointerLockElement !== canvas);
});Do not imitate pointer lock with cursor: none; that only hides the cursor.
src/protocol.ts defines protocol version 1. Because the iframe intentionally has an opaque origin,
the player validates messages by frame identity. The player supplies its origin through the
prototir_origin URL parameter so replies can use a specific target origin. The SDK accepts host
messages only from window.parent and only when their source and protocol version match.
Requires Node.js 20 or newer.
npm ci
npm run check
npm run build:cdnnpm run build produces an IIFE build, an ES module, declarations and source maps in dist.
npm run sync:review copies the build into the Prototir player and the Unity and Godot SDKs, which
bundle it for their web exports.
Bump version in package.json and CHANGELOG.md, then push to main. The deploy uploads
sdk/vX.Y.Z/ (immutable) and refreshes the sdk/v0/ channel. A version that is already on the CDN
is never overwritten: the deploy skips it with a warning, so bump the version to release changes.
Then tag vX.Y.Z and publish a GitHub release.
Runnable starter projects live in web-examples. See the creator documentation for bundle and publishing requirements.