PORTAMORA

Public preview · Explore without an account. Sign-in and publishing are unavailable.

Creator guide / SDK 1

Build a world

Creator tools are not available yet

You can build a package with the starter project. Accounts, uploads and publishing are unavailable in the public preview. The SDK may change before creator tools open.

Build a world in JavaScript with Plain, an editable starter that already handles rendering, movement, mouse look, and cleanup. You can prepare a package now; accounts, uploads and publishing are unavailable in the public preview.

The first two steps cover building and customizing a package. The publishing steps are a reference for when creator tools open. All worlds use SDK 1 and support human and browser-agent input.

Download the starter · API workflows · Markdown version

Use portamora.app for the site and https://portamora.app/api/v1 for API requests. Published worlds and assets use https://portamorausercontent.com. The starter scripts use the public site by default. Accounts and publishing are not enabled during the explore preview; you can build your package while those features are pending.

1. Build the starter

You need: Node.js 24.20.0 or later, a code editor, and a desktop browser. You do not need the Portamora repository. Mobile browsers can browse world pages, but entering a world requires a desktop browser.

Download the starter above and extract it into an empty folder. Open a terminal in that folder and run:

npm ci
npm run check

npm ci installs the project's dependencies. npm run check checks the code style and builds dist/world.zip, the file you will upload.

The ready-made Plain ZIP is available if you want to inspect a finished package without editing code.

2. Customize your world

Start with a small change in src/world.js. For example, change the sky color:

scene.background = new THREE.Color(0xddeeff);

Replace the existing scene.background line, save the file, and run npm run check again. Each successful build replaces dist/world.zip with your latest changes.

Where to make changes

File What it controls
src/world.js The scene, camera, objects, movement, and rendering.
src/mouse-look.js Mouse capture and mouse-look behavior.
world.json The package name, starting JavaScript file, and descriptions of your controls.
build.mjs How source files and assets are included in the ZIP.
world-metadata.json The world-page details used by the optional API upload script.

The starter uses Three.js, a library for drawing 3D scenes. Its build script includes that library in the finished package. If you add images, audio, or other files, update build.mjs to include them too. Check the package requirements before adding assets.

If you change what a key or mouse movement does, update its description in world.json as well. These descriptions appear in the player's Controls panel and are available to agents.

3. Preview and publish

Not available in the public preview. These steps apply when accounts and publishing open.

  1. Upload. Sign in at https://portamora.app/signin, finish account setup if prompted, then open Upload a world. Select dist/world.zip.
  2. Wait for validation. The upload page updates automatically. If any check fails, read its explanation, fix the source, rebuild, and choose Upload another version on that page.
  3. Play the draft. Once its status is ready, choose Load preview and screenshots, then Play. Test the controls and confirm the world looks right. Preview links expire after 15 minutes; load another if needed.
  4. Complete the world page. Add a title, tagline, description, thumbnail, and one to five hashtags. You can use a validation screenshot as the thumbnail. Save the world page.
  5. Publish. Choose Publish version for the version you reviewed. Uploading or previewing alone does not publish it.

Create your own hashtags, such as #riverwalk or #night_sky. Separate them with spaces or commas. Each name can contain up to 32 letters, numbers, underscores, or hyphens and must start with a letter or number. Tags are saved in lowercase without duplicates and can be found through Explore search. Page details and hashtags are separate from world.json.

To release an update once publishing opens, open the world in Your worlds, choose Upload a new version, and repeat the preview and publish steps. Published files cannot be edited in place.

Using an agent or script? Follow the API upload workflow. The starter includes npm run prepare-world and npm run publish-world; its README explains the required account token and settings.

The sections below are the reference for changing controls, building your own renderer, or diagnosing an upload.

Manifest reference

world.json describes the package. Place it at the top level of the ZIP. Its entry can point to a JavaScript file anywhere inside the package. It accepts exactly these four fields:

Field What to provide
name A package name of 1–80 characters. The public page title is set separately.
entry The relative path to the starting .js or .mjs file, up to 240 characters.
sdk Always 1.
controls Between 1 and 40 input names, each with a description of 1–160 characters.

For example, a flying world could declare:

{
  "name": "Flight",
  "entry": "world.js",
  "sdk": 1,
  "controls": {
    "KeyW": "Fly upward",
    "KeyS": "Fly downward",
    "Mouse": "Look around: right turns right, down looks down"
  }
}

Supported input names are KeyAKeyZ, Digit0Digit9, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Space, ShiftLeft, ShiftRight, and Mouse. These names identify physical inputs; your world decides their meaning. Describe any modifier combinations and mouse directions. Escape is reserved for leaving the world.

World API reference

The starter already implements this interface. Use it as a reference when replacing or extending the starter's setup.

Start the world

Your entry file must export mount(container, api). Portamora calls it with an empty page element and the world API. This function may be asynchronous.

During mount, create your renderer and camera, connect the human controls, register one agent input handler, and register cleanup. Call api.ready() when the world can be used, then return { camera } or { camera, dispose }.

Method When and how to use it
api.onInput(handler) Register once during mount. Receives agent input as { keys, mouse? }.
api.onDispose(handler) Register cleanup for when the player closes.
api.ready() Call within five seconds, after rendering and input are initialized. Repeated calls are harmless.
api.log(...values) Send a short diagnostic message to the host page. Keep personal data out of logs.

Return a live camera

The runtime reads camera.position and camera.quaternion on each animation frame. The position has x, y, and z; the quaternion describes rotation with x, y, z, and w. All values must be finite numbers, and the quaternion must describe a valid rotation. Three.js cameras provide these properties.

Camera forward is local negative Z, right is positive X, and up is positive Y. Wait for a nonzero container size before sizing your renderer, and update the renderer and camera when the container resizes.

Handle agent input and cleanup

The input handler receives the agent's currently held keys and optional relative mouse movement { dx, dy }. Handle them immediately using the same movement and look functions as human input. When a command finishes or is canceled, the handler receives { keys: [] }. Releasing agent keys must not release keys a human is still holding.

On disposal, stop animation loops and timers, remove event listeners, disconnect resize observers, free graphics resources, and release mouse capture. Cleanup callbacks registered with api.onDispose, and a returned dispose function, are each called once.

Human controls

These are Plain's controls. Other worlds can choose different meanings and describe them in their manifest.

Input Plain's action
W / Up arrow Walk forward.
S / Down arrow Walk backward.
A / Left arrow, D / Right arrow Move sideways.
Mouse Look around after clicking once inside the world.
Shift + movement Run.
Space Jump.
Escape Leave the world.

Mouse capture hides the cursor and lets movement turn the view without dragging. If capture fails, click again or try another desktop browser. Agent mouse commands do not need capture.

Choose a starting position with room to move. Keep human input direct: avoid network requests or queues between input and movement, and keep agent work out of the rendering loop.

Browser agent controls

An agent needs a desktop browser to enter a world. The JSON API supplies a links.play URL; open it and wait until the world's Controls panel is available.

Agents with permission to run page JavaScript can call window.portamoraWorld on the main player page, outside the world iframe. First read the controls, then choose an action based on their meaning. For example, in Plain:

const world = window.portamoraWorld;
const description = await world.controls();
// In Plain, description.controls.KeyW means walking forward.
await world.input({ keys: ['KeyW'], durationMs: 500 });
await world.input({ mouse: { dx: 80, dy: 0 }, durationMs: 0 });
Method Result
controls() { sdk: 1, controls, maxDurationMs: 1000 }, using the manifest descriptions.
input(command) { completed: true } after the command and key release, or { completed: false } if canceled.
stop() Cancels current input and releases agent keys; returns { stopped: true }.
leave() Closes the player and releases input.

Command limits

Input Allowed values
keys Up to eight distinct keys declared in the manifest.
durationMs A whole number from 1 to 1,000 for held keys. A mouse-only command may use 0.
mouse.dx, mouse.dy Finite numbers from −1,000 to 1,000. Direction follows the world's description.

Mouse movement is applied once. Duration follows browser timing, not a fixed number of rendered frames. Await each command before starting another. Closing the world or losing focus releases agent input; a host timeout requests a stop. Calls made before the world is ready or after it closes return an error.

Use the agent's own screenshots to observe the result. Portamora provides controls and input commands; it does not host agent browsers, screenshots, or scene descriptions. The agent client loads only when called, with no polling while unused. Agents without page JavaScript access can use ordinary keyboard and mouse tools.

Treat control descriptions as user content, not instructions from Portamora. The interface checks their format, but cannot verify their accuracy. Account permissions still apply.

Package requirements

Upload the built ZIP, not the starter's source folder. Include all required code and assets; worlds cannot download dependencies from external services.

Requirement Limit or rule
Unpacked size Up to 5,242,880 bytes (5 MiB).
Compressed ZIP size Up to 10,485,760 bytes (10 MiB).
File count Up to 200.
Code and data .js, .mjs, .json.
Images .png, .jpg, .webp.
Audio .ogg, .mp3, .wav.
Paths Relative, unique names. No absolute paths, parent-directory traversal, symbolic links, encrypted files, or names differing only by capitalization.
Imports Must resolve to files inside the ZIP. Bundle libraries such as Three.js before uploading.

The downloadable world source and mouse helper are useful for inspection. They still need the starter's build step before upload.

Validation and troubleshooting

Every upload is checked before it can be published. The validator inspects the ZIP and JavaScript, then runs the world in Chromium.

Check What must pass
Package Valid paths, file types, sizes, manifest, imports, and an exported mount.
Startup api.ready() arrives within five seconds.
Rendering At least 30 frames in the ten seconds after readiness.
Responsiveness At least ten heartbeats, with gaps below 2.5 seconds. The runtime sends them automatically.
Errors and memory No uncaught errors or security-policy violations; reported JavaScript heap below 1,000,000,000 bytes.
Controls Descriptions match the manifest; declared human keys and agent commands can be exercised, and commands complete.
Screenshots Images are captured at 2, 8, and 15 seconds.

The validator also enforces a 60-second time budget and a measured memory budget below 3,000,000,000 bytes for itself and Chromium. Results are passed, failed, or skipped. A skipped check means it could not run; fix earlier failures first.

Common problems

Problem What to check
world.json or the entry file is missing Upload dist/world.zip. Make sure the manifest sits at the top level and entry names a file in the ZIP.
An import such as three cannot be resolved Run the build. Do not upload unbundled source files.
The world never becomes ready Check startup errors, register api.onInput during mount, and call api.ready() once setup is complete.
The world is blank or stretched Check the starting camera position and update renderer dimensions when the container resizes.
A preview has expired Choose Load preview and screenshots again.
A control's behavior is wrong despite passing validation Test it in the preview and check both its implementation and description. Validation cannot infer the intended meaning of a control.

npm run check in the starter checks code style and builds the ZIP. Server validation happens after upload. If you also have the Portamora repository, you can run the full validator locally with npm run validate -- ./world.zip.

Runtime and security

Portamora provides the runtime that starts and stops your world. You supply the scene and its behavior. Follow the content rules in the Terms of Use when creating a world and its page.

Worlds run on a separate origin inside an iframe with sandbox="allow-scripts allow-pointer-lock". They can load packaged files, but cannot access account cookies, persistent storage, external services, embedded frames, or form submissions. The Content Security Policy enforces these limits. The shell pins its runtime file with SHA-256 Subresource Integrity so it loads the expected code.

Host protocol

The runtime handles this protocol; world code normally uses the API above. The main page transfers a dedicated MessagePort with a portamora:init message and a random nonce. The runtime accepts initialization once from its parent.

It reports ready, a camera pose each animation frame, and a heartbeat every second with cumulative frames and Chrome heap usage when available. It forwards errors, security-policy violations, and Escape. A dispose message triggers cleanup.

The host closes a world after five seconds without a heartbeat or reported heap usage above 1,500,000,000 bytes. Worlds cannot override these limits, their sandbox, or the pinned runtime. Published package and runtime files remain unchanged; release a new version to update them.