Scripts
IMPORTANT
As Hyperfy is in alpha, the scripting API is likely to evolve fast with breaking changes. This means your apps can and will break as you upgrade worlds. Once scripting is stable we'll move toward a forward compatible model, which will allow apps to be shared/traded with more confidence that they will continue to run correctly.
Lifecycle
App scripts execute in every environment (server + each client). The top-level module code runs once per build in that environment, so treat it as initialization.
Use world.isServer and world.isClient to split logic, and store shared server state on app.state so late-joining clients can initialize correctly.
Update events:
app.on('fixedUpdate', ...)for fixed timestep logicapp.on('update', ...)for per-frame logicapp.on('lateUpdate', ...)for post-frame logicapp.on('animate', ...)for distance-based animation ticks
Cleanup:
app.on('destroy', ...)fires when an app is rebuilt or removed. Unsubscribe events, release controls, and clear timers there.
Apps
Apps power Hyperfy's content. You can think of them as a combination of a model and a script. They can talk to eachother, and run both on the client and the server. Apps have a UI to configure properties in the scripts, and can load additional models inside of them.
Nodes
Apps are made up of a hierarchy of nodes that you can view and modify within the app runtime using scripts.
The gltf model that each app is based on is automatically converted into nodes and inserted into the app runtime for you to interact with.
Certain node types can also be created and used on the fly using app.create(nodeName).
World
The World API access methods and properties outside of the Apps, like players, networking or managing nodes outside of the local hierarchy.
Utils
The Utils documentation provides a set of miscellaneous globals available in the scripting environment, like a random number generator and access to some three.js methods.
Networking
Hyperfy Networking happens inside of Apps, using methods from both the App and World APIs. You can either send events between the client and server on the same app, or send messages to external apps on the server.
Script Formats and Imports
App scripts are uploaded as a folder of files (no bundling). The entry defaults to apps/<AppName>/index.js unless scriptEntry is set in the blueprint. scriptFiles maps app-relative paths to asset URLs, and scriptFormat tells the runtime how to interpret the entry file.
Entry formats:
module: the entry file mustexport default (world, app, fetch, props, setTimeout) => { ... }.legacy-body: keep the classic body-style entry (noexport). Imports must be at the top of the entry file. The runtime wraps it intoexport default (...) => { ... }. New apps default toscriptFormat: "module".
If scriptFormat is missing, app-server infers it during deploy:
modulewhen the entry exports default.legacy-bodyotherwise (with a warning). The blueprint JSON is not modified.
Import rules (all formats):
- Relative imports only (
./or../) inside the same app folder. - Shared imports via
@shared/...orshared/...from the project-levelshared/folder. - No bare imports (
react,lodash), no node builtins, no cross-app imports.
Migration
Legacy single-file scripts remain supported without any changes. To opt into multi-file modules:
Legacy-body (minimal change):
- Add
"scriptFormat": "legacy-body"to your app's blueprint JSON (or rungamedev scripts migrate --legacy-body). - Keep your existing
index.jsbody-style entry and move helpers into new.jsfiles with relative imports. - Run app-server or
gamedev apps deploy <app>.
Module (full ESM):
- Add
"scriptFormat": "module"to your app's blueprint JSON (or rungamedev scripts migrate --module). - Update
index.jstoexport defaulta function with the same signature. - Move shared logic into modules and use relative imports.
gamedev scripts migrate --modulecan also wrap legacy-body entries for you when possible.
Bundling is removed. If you relied on bare imports or node builtins, refactor to local modules.