# Feature matrix (/docs/feature-matrix) This is the answer to *"does this work on Unreal?"* The [Platform API](/docs/platform-api) is identical across runtimes by design β€” the differences are confined to rendering, input, and networking. We track them here rather than scattering "N/A on this runtime" notes through the reference. The Web SDK is the frozen contract. A βœ… in the Web column is the source of truth; the Native column reflects mirror status. Anything not yet at parity is marked 🟑. ## Platform API [#platform-api] | Capability | Web | Native (Unreal) | | --------------------------------------------------------- | :-: | :-------------: | | Authentication & Identity (`Helix.auth`, `Helix.profile`) | βœ… | βœ… | | LIX & Economy (`Helix.wallet`, `Helix.marketplace`) | βœ… | βœ… | | Cloud Save (`Helix.cloudSave`) | βœ… | βœ… | | Memory Store (`Helix.memoryStore`) | βœ… | βœ… | | Inventory & Items (`Helix.inventory`) | βœ… | βœ… | | Social & Presence (`Helix.social`) | βœ… | βœ… | | Chat (`Helix.chat`) | βœ… | βœ… | | Voice / proximity (`Helix.voice`) | βœ… | βœ… | | Avatars (`Helix.avatar`) | βœ… | βœ… | | Analytics (`Helix.analytics`) | βœ… | βœ… | ## Runtime-specific [#runtime-specific] | Capability | Web | Native (Unreal) | | ------------------------------------------------ | :-----------------------: | :------------------------------: | | Instant play from a link (no install) | βœ… | β€” | | HELIX multiplayer & networking (`Helix.network`) | βœ… | β€” | | Engine-native dedicated-server networking | β€” | βœ… | | Replication wrappers | β€” | βœ… | | Rendering / scene graph | Web stack (Three.js etc.) | Unreal | | Languages | TypeScript / JS | C++ Β· Blueprint Β· PuerTS Β· UnLua | Legend: βœ… available Β· 🟑 in progress Β· β€” not applicable to this runtime. A CI check compares the generated [Web reference](/docs/web-sdk/reference) against the generated [Native reference](/docs/native-sdk/reference) and flags any Platform API method that exists on one but not the other. This matrix is generated from that check, so it can't silently drift. # HELIX 3 Documentation (/docs) HELIX is an **engine-agnostic virtual-world platform**. You build a world once against a single SDK and HELIX gives you identity, the LIX economy, storage, inventory, social, voice, and multiplayer β€” then runs it on any device from a single link. These docs are organized around one idea: The **Platform API** is engine-agnostic and documented **once**. The **Web SDK** (TypeScript) is the canonical contract; the **Native (Unreal) SDK** mirrors it 1:1 in C++, Blueprint, PuerTS, and UnLua. The same function names mean the same thing everywhere. ## Start here [#start-here] ## The map [#the-map] ## Built for humans and agents [#built-for-humans-and-agents] Every page in these docs is available as clean Markdown for LLMs β€” append `.md`-style content routes, or pull the whole corpus: * [`/llms.txt`](/llms.txt) β€” a structured index of every page. * [`/llms-full.txt`](/llms-full.txt) β€” the entire documentation as one Markdown file. * Use the **Copy Markdown** button at the top of any page to grab that page for an AI agent. We expect most worlds to be built by AI coding agents calling this SDK. The docs are written to be read by both. # Migrating from HELIX 2 (/docs/migrating-from-helix2) HELIX 3 is a new product, not an iteration of HELIX 2. If you built on HELIX 2 β€” QBCore/Lua scripts, FiveM-style roleplay servers, the Lua API or HelixJS β€” this page explains what carries over and what doesn't. The rest of these docs describe HELIX 3 only. HELIX 2 (QBCore, the Lua class API, Creator Kit, Blueprint-as-primary) is being wound down. Treat it as legacy; build new worlds on the [unified SDK](/docs/platform-api). ## What changed at the top [#what-changed-at-the-top] | HELIX 2 | HELIX 3 | | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Two siloed scripting layers (Lua API **and** HelixJS) with no shared model | One **unified SDK**: an engine-agnostic [Platform API](/docs/platform-api) + per-runtime SDKs that mirror it 1:1 | | QBCore/Lua FiveM-compat framework as a first-class surface | **Removed.** QBCore is legacy; not part of HELIX 3 | | Engine-specific, UE5-centric (Creator Kit, Blueprint-first) | **Engine-agnostic.** Web is canonical; Unreal mirrors it | | `HInventory` (placeholder), ad-hoc persistence | First-class [Inventory](/docs/platform-api/inventory) + [Cloud Save](/docs/platform-api/cloud-save) / [Memory Store](/docs/platform-api/memory-store) | | `helixId` mentioned but never explained | Full [Authentication & Identity](/docs/platform-api/authentication) model | ## Concept mapping [#concept-mapping] | HELIX 2 | HELIX 3 | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | `HPlayer`, `helixId` | [`Helix.auth` / `Helix.profile`](/docs/platform-api/authentication) | | Lua `TriggerServerEvent` / HelixJS `Helix.endpoint()` | [`Helix.network`](/docs/web-sdk/multiplayer) (`send` / `broadcast` / `request`) | | Lua `Database`, `HPlayer:SetValue/GetValue`, HelixJS `State` | [Cloud Save](/docs/platform-api/cloud-save) (durable) + [Memory Store](/docs/platform-api/memory-store) (volatile) | | `HInventory` (unfinished) | [`Helix.inventory`](/docs/platform-api/inventory) + Universal Item Contract | | In-world purchases (concept only) | [`Helix.wallet` / `Helix.marketplace`](/docs/platform-api/lix-economy) | | WebUI bridge (Lua/JS ↔ HTML) | `Helix.ui` + standard web UI in the web runtime | | QBCore packages (qb-inventory, qb-banking, …) | Community packages on top of the Platform API β€” not core docs | ## What's deprecated [#whats-deprecated] * **QBCore / FiveM compatibility layer** β€” entirely. * **The HELIX 2 Lua class API** (`HVehicle`, `HCharacter`, etc. as documented in HELIX 2) β€” replaced by the unified SDK surface. * **Creator Kit** and the UE5-as-primary workflow β€” superseded by HELIX Studio and the [Native SDK](/docs/native-sdk). * **Point-in-time migration guides** (e.g. 5.5β†’5.7) β€” no longer relevant. ## What carries over (conceptually) [#what-carries-over-conceptually] * The **client/server authority** model β€” now formalized as the [golden rule](/docs/introduction/how-helix-works#server-authority--the-golden-rule). * **Packages & the Vault** distribution idea β€” now built on the Platform API. * The **LIX** economy β€” now a first-class, server-authoritative [API](/docs/platform-api/lix-economy). ## How to move a world [#how-to-move-a-world] **Re-target the runtime.** Pick [Web or Native](/docs/introduction/choose-your-runtime). Most HELIX 2 worlds were UE5 β€” Native is the closest home, but consider Web for reach. **Replace the data layer.** Move ad-hoc Lua/SQLite persistence to [Cloud Save](/docs/platform-api/cloud-save) (durable) and [Memory Store](/docs/platform-api/memory-store) (transient). **Rewrite events** against [`Helix.network`](/docs/web-sdk/multiplayer) (web) or Unreal replication with [HELIX wrappers](/docs/native-sdk/networking) (native). **Move value logic server-side.** Anything QBCore did client-side for money/items must become server-authoritative under the [Inventory](/docs/platform-api/inventory) tiers. Start from a template (`helix create --template ...`) rather than porting file by file β€” the shapes are different enough that a fresh scaffold is usually faster. # HELIX CLI (/docs/cli) The `@helix/cli` is the front door for building worlds. It scaffolds projects, runs them locally with a real instance, validates against platform rules, and deploys immutable builds. ```bash npm install -g @helix/cli # or: npx @helix/cli ``` ## Commands [#commands] ### Typical loop [#typical-loop] ```bash npx @helix/cli create my-world --template multiplayer-starter cd my-world && npm install helix dev # iterate helix validate # catch issues early helix deploy # ship a build, get a link ``` ## Templates [#templates] Templates are starting points wired with the SDK, a server `HelixInstance`, and sensible manifest scopes. Start from one rather than an empty directory. ## Auto-generated reference [#auto-generated-reference] The CLI is also where the docs pipeline lives. On every merge to `main`: The **Web SDK reference** is generated from the `@helix/sdk` TypeScript source with **TypeDoc**. The **Native SDK reference** is generated from the Unreal plugin headers with **Doxygen**, then mapped to Blueprint / PuerTS / UnLua. The [`/llms.txt`](/llms.txt) index and [`/llms-full.txt`](/llms-full.txt) bundle are rebuilt, and the site is deployed. This is what keeps [Web reference](/docs/web-sdk/reference) and [Native reference](/docs/native-sdk/reference) in sync with shipped code. See the repo's `.github/workflows/deploy.yml` and `CONTRIBUTING.md` for the exact pipeline. ## For AI agents [#for-ai-agents] Agents drive the same CLI β€” and get a richer interface through the [MCP server](/docs/cli/mcp), which exposes discovery, validation, and publishing as tools. # MCP server for AI agents (/docs/cli/mcp) HELIX expects most worlds to be built by AI agents. The **MCP server** (`@helix/mcp`) gives an agent a structured, typed interface to the platform β€” discovery, validation, and publishing as callable tools β€” instead of scraping a CLI's stdout. Anything a human can do through the [CLI](/docs/cli), an agent can do through MCP. The two are deliberately at parity. ## What it exposes [#what-it-exposes] * **Discovery** β€” list available systems, abilities, templates, and package manifests so the agent builds on what already exists instead of reinventing it. * **Validation** β€” check a world against the manifest schema and platform rules before publishing. * **Publishing** β€” create builds and publish worlds. * **Docs** β€” read these docs as structured Markdown (the same content behind [`/llms.txt`](/llms.txt)). ## Connect it [#connect-it] Point your agent host at the server (example for a Claude/Cursor-style MCP config): ```json { "mcpServers": { "helix": { "command": "npx", "args": ["-y", "@helix/mcp"] } } } ``` ## Why agents like these docs [#why-agents-like-these-docs] These docs are built to be machine-readable from day one: * Every page is available as clean Markdown (the **Copy Markdown** button, and content routes). * [`/llms.txt`](/llms.txt) is a structured index of the whole site. * [`/llms-full.txt`](/llms-full.txt) is the entire corpus in one file for context windows. * Every parameter and error is typed and described β€” what's obvious to a human reader is spelled out for an LLM. Together with the MCP server, an agent can read the docs, scaffold a world, validate it, and publish it without a human in the loop. # Build your first multiplayer world (/docs/guides/first-multiplayer-world) This builds a tiny but real multiplayer world: players see each other, can wave, and can press a button that the **server** validates before rewarding. Uses [Instances & Events](/docs/web-sdk/multiplayer) and the [server authority](/docs/introduction/how-helix-works#server-authority--the-golden-rule) rule. ## 1. Boot and join [#1-boot-and-join] ```ts title="src/client.ts" import { Helix } from '@helix/sdk'; await Helix.init({ worldId: window.HELIX_WORLD_ID, launchTicket: window.HELIX_LAUNCH_TICKET, }); const instance = await Helix.instance.join(); instance.onPlayerJoined((p) => spawnAvatar(p)); instance.onPlayerLeft((p) => removeAvatar(p)); for (const p of instance.getPlayers()) spawnAvatar(p); ``` ## 2. A cosmetic event (client-trusted) [#2-a-cosmetic-event-client-trusted] Waving doesn't affect value, so the client can drive it. ```ts title="src/client.ts" Helix.network.on('wave', ({ from }) => playWave(from)); document.querySelector('#wave')!.addEventListener('click', () => { Helix.network.send('wave', {}); }); ``` ```ts title="src/server.ts" import { HelixInstance } from '@helix/server-sdk'; export default class MyWorld extends HelixInstance { async onEvent(player, event) { if (event === 'wave') this.broadcast('wave', { from: player.id }); } } ``` ## 3. A value event (server-authoritative) [#3-a-value-event-server-authoritative] A "daily bonus" button grants LIX-adjacent value β€” so the **server** decides, enforces the once-per-day rule, and grants. The client only asks. ```ts title="src/client.ts" const res = await Helix.network.request('claim-bonus', {}); if (res.ok) showToast(`+${res.amount} coins!`); else showToast(res.reason); ``` ```ts title="src/server.ts" export default class MyWorld extends HelixInstance { async onEvent(player, event) { if (event === 'wave') this.broadcast('wave', { from: player.id }); } // request/response handler async onRequest(player, event) { if (event !== 'claim-bonus') return; const key = `bonus:${player.id}:${today()}`; const already = await Helix.cloudSave.get(key); if (already) return { ok: false, reason: 'Already claimed today' }; await Helix.cloudSave.set(key, true); // durable, authoritative await this.grantCoins(player.id, 50); // server-side grant return { ok: true, amount: 50 }; } } ``` If the client could grant its own bonus, players would script infinite coins. The wave is harmless client-side; the bonus is decided server-side and recorded in [Cloud Save](/docs/platform-api/cloud-save). That's the [golden rule](/docs/introduction/how-helix-works#server-authority--the-golden-rule) in two handlers. ## 4. Publish [#4-publish] ```bash helix validate && helix deploy ``` Share the link. Open it in two browsers β€” you'll see two players, waves replicate, and the bonus is claimable once per day per player. ## Next [#next] # Guides (/docs/guides) Concept pages explain *what* something is; guides show *how* to build a specific thing end to end. Each one names the [Platform API](/docs/platform-api) surfaces it uses and, where it matters, shows the same task on both runtimes. This section grows with the platform. Proximity voice, avatars, and matchmaking guides are next. If you're an AI agent, the [`/llms-full.txt`](/llms-full.txt) bundle includes every guide in one file. # Persist player data (/docs/guides/persist-player-data) Uses [Cloud Save](/docs/platform-api/cloud-save) (durable) and, for hot counters, [Memory Store](/docs/platform-api/memory-store) (volatile). ## Load on join, save on change [#load-on-join-save-on-change] ```ts title="src/server.ts" import { HelixInstance } from '@helix/server-sdk'; type Progress = { level: number; coins: number }; const DEFAULT: Progress = { level: 1, coins: 0 }; export default class MyWorld extends HelixInstance { async onPlayerJoin(player) { const p = (await Helix.cloudSave.get(key(player.id))) ?? DEFAULT; this.setState(player.id, p); } async onPlayerLeave(player) { await Helix.cloudSave.set(key(player.id), this.getState(player.id)); } } const key = (id: string) => `progress:${id}`; ``` Never let a client write its own `coins`. Loading and saving value-bearing state lives in server-authoritative code. Clients send *intent* (events); the server updates and persists. ## Concurrent edits: read–modify–write [#concurrent-edits-readmodifywrite] Two instances of your world can run at once. For a value many players or sessions can change, don't blind-write β€” read, modify, and write inside a server-guarded step: ```ts async function addCoins(playerId: string, delta: number) { const cur = (await Helix.cloudSave.get(key(playerId))) ?? DEFAULT; const next = { ...cur, coins: cur.coins + delta }; await Helix.cloudSave.set(key(playerId), next); return next.coins; } ``` For **hot** counters (e.g. "players in this zone right now") that don't need durability, use Memory Store's atomic `increment` and only flush a summary to Cloud Save occasionally: ```ts const inZone = await Helix.memoryStore.increment(`zone:${zoneId}`, 1, 60); ``` ## Choosing the store [#choosing-the-store] | Data | Store | Why | | ------------------------------- | ----------------------------------------------- | ------------------------ | | Progress, settings, owned state | [Cloud Save](/docs/platform-api/cloud-save) | Must survive sessions | | Live leaderboard / matchmaking | [Memory Store](/docs/platform-api/memory-store) | Fast, shared, disposable | | Hot counters | Memory Store `increment` | Atomic, cheap | ## Next [#next] # Sell an item for LIX (/docs/guides/sell-item-for-lix) This wires a real [in-world purchase](/docs/platform-api/lix-economy#in-world-purchases-iwp). The rule that shapes everything: **the client requests, the server settles.** LIX is deducted server-side and the grant is idempotent so retries can't double-charge. ## The flow [#the-flow] **Client requests** a purchase. It never touches the wallet directly. **Server validates** (price, eligibility, stock), then **consumes/charges and grants atomically** with an idempotency key. **Wallet & inventory update**; the client reacts to the settled result. ## Simple catalog purchase [#simple-catalog-purchase] For items priced in the platform catalog, one call does it β€” settlement is handled for you: ```ts const result = await Helix.marketplace.purchaseItem('premium_sword_001'); if (result.status === 'completed') equip('premium_sword_001'); else showToast(result.reason); ``` ```ts const result = await Helix.marketplace.purchaseItem('premium_sword_001'); if (result.status === 'completed') equip('premium_sword_001'); ``` ```lua local result = Helix.marketplace.purchaseItem("premium_sword_001") if result.status == "completed" then equip("premium_sword_001") end ``` ## Custom server-authoritative sale [#custom-server-authoritative-sale] When you need custom pricing or bundles, the client asks and your `HelixInstance` server settles: ```ts title="src/client.ts" const res = await Helix.network.request('buy', { sku: 'starter-bundle' }); if (res.ok) showInventory(); else showToast(res.reason); ``` ```ts title="src/server.ts" export default class Shop extends HelixInstance { async onRequest(player, event, payload) { if (event !== 'buy') return; const sku = CATALOG[payload.sku]; if (!sku) return { ok: false, reason: 'Unknown item' }; const { lix } = await Helix.wallet.getBalance(player.id); if (lix < sku.priceLix) return { ok: false, reason: 'Not enough LIX' }; // atomic: charge LIX + grant item, idempotent on requestId const result = await Helix.inventory.purchase({ buyer: player.id, itemId: sku.itemId, priceLix: sku.priceLix, idempotencyKey: this.requestId, }); return result.ok ? { ok: true } : { ok: false, reason: result.error }; } } ``` Networks retry. Without an `idempotencyKey`, a retried purchase can charge twice. The SDK provides a stable key per request β€” pass it through. The grant and the charge are one atomic operation. ## What the platform handles for you [#what-the-platform-handles-for-you] * **Settlement & fees.** Commission and payouts are applied by the platform β€” you don't compute them. * **Anti-fraud & limits.** Server-side checks live in the economy service. * **The provider stays hidden.** You call `Helix.wallet` / `Helix.marketplace` / `Helix.inventory`; payment internals are never in your code. ## Related [#related] # Choose your runtime (/docs/introduction/choose-your-runtime) You write your world logic against the engine-agnostic [Platform API](/docs/platform-api) either way. The runtime decides how it renders, how it networks, and which languages you write in. ## Quick guide [#quick-guide] ## Side by side [#side-by-side] | | Web | Native (Unreal) | | ---------------- | -------------------------------------------------------- | ---------------------------------------- | | **Languages** | TypeScript / JavaScript | C++, Blueprint, PuerTS (TS), UnLua (Lua) | | **Distribution** | Instant, from a link, any device | Engine-native build | | **Multiplayer** | HELIX networking ([web-only](/docs/web-sdk/multiplayer)) | Engine-native dedicated servers | | **Platform API** | βœ… identical | βœ… identical | | **Status** | Canonical contract, available first | Mirrors the web contract 1:1 | | **Best for** | Reach, speed, AI-built worlds | Fidelity, large maps, existing UE teams | You are **not** locked in. Because both runtimes call the same Platform API, world logic written for web ports to Native with minimal change β€” the differences are rendering, input, and networking, not identity/economy/storage. ## Still deciding? [#still-deciding] Start on **Web**. It's the canonical surface, it's the fastest path to a playable link, and everything you learn transfers. Move to Native when you need engine-level fidelity or you already have an Unreal team. Continue to the [Quickstart](/docs/introduction/quickstart). # How HELIX works (/docs/introduction/how-helix-works) HELIX has a small, deliberate vocabulary. Learn these five nouns and the rest of the docs read cleanly. **World Β· Build Β· Instance Β· Player Β· Event.** These are the only words the SDK and docs use for the runtime. Provider-specific terms (rooms, matches, servers) never appear in the public API β€” they're implementation details HELIX hides behind the SDK. ## The nouns [#the-nouns] ## The lifecycle [#the-lifecycle] What happens when someone opens your world from a link: **Launch.** A player opens your World. HELIX authenticates them (or spins up an instant guest) and issues a short-lived **Launch Ticket**. **Init.** Your client boots and calls `Helix.init({ worldId, launchTicket })`. The SDK validates the ticket and exposes the player's identity, wallet, and inventory. **Join an Instance.** The player joins a running Instance of the current Build (or HELIX creates one). The platform issues an **Instance Ticket** that the runtime verifies before admitting them. **Play.** Client and server exchange [Events](/docs/web-sdk/multiplayer). The server is authoritative for anything that touches wallet, inventory, or ownership; the client is trusted only for visuals and local gameplay. **Persist.** State that must survive the session is written through [Cloud Save](/docs/platform-api/cloud-save). Transient cross-instance state (leaderboards, matchmaking) uses [Memory Store](/docs/platform-api/memory-store). ## Server authority β€” the golden rule [#server-authority--the-golden-rule] If an action affects **inventory, currency, or ownership**, the **server is authoritative**. If it only affects **visuals or local gameplay**, the client can be trusted. This single rule decides where every piece of your logic belongs β€” see the [Universal Item Contract](/docs/platform-api/inventory). ## Where this lives in the SDK [#where-this-lives-in-the-sdk] You orchestrate all of this from the [Web SDK](/docs/web-sdk) (client) and the server SDK (`HelixInstance`). The platform pieces β€” auth, wallet, storage, inventory β€” are the [Platform API](/docs/platform-api), identical across runtimes. # What is HELIX 3? (/docs/introduction) HELIX is a **virtual-world platform**. It is not a game engine and it is not a single game β€” it is the layer that sits **on top of** a rendering engine and provides everything a multiplayer virtual world needs but a renderer doesn't: * **Identity** β€” one account across every world, with instant guest play. * **Economy** β€” a real currency (**LIX**), a marketplace, and creator payouts. * **Persistence** β€” durable Cloud Save and volatile Memory Store. * **Items & inventory** β€” universal items that work across worlds and runtimes. * **Social** β€” friends, presence, invites, chat, and proximity voice. * **Multiplayer** β€” instances, networking, and server-authoritative state. * **Hosting & distribution** β€” your world is playable instantly, on any device, from a link. You reach all of it through **one SDK**. HELIX is an AI-native, engine-agnostic virtual-world platform β€” *"Roblox for adults, on the web."* Creation no longer has to happen inside a proprietary studio. AI agents become the studio; HELIX becomes the runtime, economy, identity, and distribution layer. ## One platform, two runtimes [#one-platform-two-runtimes] This is the core idea, and it shapes the whole of these docs.
Platform (engine-agnostic)

Identity, LIX, storage, inventory, marketplace, social, analytics, moderation. The same backend serves every runtime. Documented once, in the Platform API.

Runtime (engine-specific)

Where your world actually renders and runs. Web (TypeScript, the canonical contract) and Native (Unreal: C++, Blueprint, PuerTS, UnLua) both call the same Platform API.

The **Web SDK is the canonical, frozen contract.** The Native runtime mirrors it 1:1 β€” every function, including character and world functions. If you learn the API on one runtime, you know it on the other. See [Platform vs Engine SDK](/docs/introduction/platform-vs-engine) for the full mental model. ## Who HELIX is for [#who-helix-is-for] * **AI-assisted creators** shipping multiplayer web-3D experiences who don't want to build identity, economy, or netcode from scratch. * **Studios and teams** that want their world on web today and on Unreal later β€” without rewriting game logic against a different API. * **AI agents** building worlds autonomously through the [CLI](/docs/cli) and [MCP server](/docs/cli/mcp). ## Next [#next] # Platform vs Engine SDK (/docs/introduction/platform-vs-engine) Everything in the HELIX SDK falls into one of two buckets. Knowing which bucket you're in tells you where to find the docs, whether the code is portable, and what to expect on another runtime. ## Platform API β€” engine-agnostic [#platform-api--engine-agnostic] The **Platform API** is the shared layer. It has nothing to do with rendering. It's the same behavior, the same guarantees, and the same function names on every runtime: | Capability | Namespace | Docs | | ------------------------- | ----------------------------------- | --------------------------------------------------- | | Authentication & identity | `Helix.auth`, `Helix.profile` | [Authentication](/docs/platform-api/authentication) | | LIX & economy | `Helix.wallet`, `Helix.marketplace` | [LIX & economy](/docs/platform-api/lix-economy) | | Durable storage | `Helix.cloudSave` | [Cloud Save](/docs/platform-api/cloud-save) | | Volatile storage | `Helix.memoryStore` | [Memory Store](/docs/platform-api/memory-store) | | Items & inventory | `Helix.inventory` | [Inventory](/docs/platform-api/inventory) | | Social & presence | `Helix.social` | [Social](/docs/platform-api/social) | Write against the Platform API and your logic is **portable**: it behaves identically whether your world runs on the web or inside Unreal. ## Engine-specific runtime [#engine-specific-runtime] A **runtime** is where your world actually renders and executes. Runtimes add the things only an engine can provide β€” scene graph, input, physics, rendering β€” plus engine-flavored conveniences. * **[Web SDK](/docs/web-sdk)** β€” TypeScript. This is the **canonical contract**. It also ships a full **multiplayer & networking** API (HELIX's own implementation) that is web-specific. * **[Native (Unreal) SDK](/docs/native-sdk)** β€” the same Platform API exposed to **C++**, **Blueprint**, **PuerTS** (TypeScript) and **UnLua** (Lua), plus engine-specific helpers like replication wrappers. Native worlds use the engine's own networking rather than the web stack. The Web TypeScript SDK is frozen first and treated as the canonical API surface. The Native runtime **mirrors it 1:1** β€” every function, including character and world functions. When the two ever differ, the web signature wins and the difference is documented in the [feature matrix](/docs/feature-matrix). ## The same call, five ways [#the-same-call-five-ways] Because the Platform API is identical across runtimes, the *same* operation looks like idiomatic code in each language. Here's reading the current player's LIX balance: ```ts const wallet = await Helix.wallet.getBalance(); console.log(`You have ${wallet.lix} LIX`); ``` ```cpp UHelixWallet* Wallet = UHelix::Get()->Wallet(); Wallet->GetBalance(FOnBalance::CreateLambda([](const FHelixBalance& B) { UE_LOG(LogHelix, Display, TEXT("You have %lld LIX"), B.Lix); })); ``` ```text Helix β†’ Wallet β†’ Get Balance (latent node) ↳ On Success β†’ Break Helix Balance β†’ "Lix" β†’ Print String ``` ```ts // PuerTS runs the same TypeScript API as the Web SDK, inside Unreal. const wallet = await Helix.wallet.getBalance(); console.log(`You have ${wallet.lix} LIX`); ``` ```lua local wallet = Helix.wallet.getBalance() UE.Log(string.format("You have %d LIX", wallet.lix)) ``` This `groupId` is shared across the docs: pick **PuerTS** once and every runtime-tabbed example on every page follows you. ## How to use this distinction [#how-to-use-this-distinction] * Reading about a **concept** (auth, LIX, storage)? You're in the [Platform API](/docs/platform-api). It applies to every runtime. * Reading about **rendering, input, networking, or engine conveniences**? You're in a runtime section β€” [Web SDK](/docs/web-sdk) or [Native SDK](/docs/native-sdk). # Quickstart (/docs/introduction/quickstart) This builds and publishes a real multiplayer world on the **Web** runtime β€” the fastest path to a playable link. Native (Unreal) setup lives in the [Native SDK](/docs/native-sdk) section. ## Prerequisites [#prerequisites] * Node.js 20+ * A HELIX account ([helixgame.com](https://helixgame.com)) ## 1. Create a world [#1-create-a-world] **Scaffold from a template.** ```bash npx @helix/cli create my-world --template multiplayer-starter cd my-world npm install ``` **Run it locally.** This starts the world with a local instance and hot reload. ```bash helix dev ``` Open the printed URL. You're now in your world as an authenticated player. ## 2. Write a little logic [#2-write-a-little-logic] Client code talks to the [Platform API](/docs/platform-api) and to your server via [Events](/docs/web-sdk/multiplayer). Here we greet the player and read their balance: ```ts title="src/client.ts" import { Helix } from '@helix/sdk'; await Helix.init({ worldId: window.HELIX_WORLD_ID, launchTicket: window.HELIX_LAUNCH_TICKET, }); const me = await Helix.profile.getMyProfile(); const wallet = await Helix.wallet.getBalance(); console.log(`Welcome, ${me.displayName} β€” you have ${wallet.lix} LIX`); ``` Server code extends `HelixInstance` and owns authoritative state: ```ts title="src/server.ts" import { HelixInstance } from '@helix/server-sdk'; export default class MyInstance extends HelixInstance { async onPlayerJoin(player) { this.broadcast('player-joined', { id: player.id, name: player.displayName }); } async onEvent(player, event, payload) { if (event === 'wave') this.broadcast('wave', { from: player.id }); } } ``` ## 3. Publish & play [#3-publish--play] **Validate** your world against the platform manifest and rules: ```bash helix validate ``` **Deploy.** This creates an immutable Build and makes it playable from a link. ```bash helix deploy ``` **Share the link.** Anyone can open it and play instantly β€” as a guest if they're not signed in. ## Where to go next [#where-to-go-next] # Native (Unreal) SDK (/docs/native-sdk) The Native SDK brings HELIX to **Unreal Engine**. It exposes the exact same [Platform API](/docs/platform-api) as the web β€” identity, LIX, storage, inventory, social β€” and adds engine-native rendering, physics, and networking. Native worlds are the high-fidelity path. The Native SDK **mirrors the [Web SDK](/docs/web-sdk) exactly** β€” every function, including character and world functions. If a method exists on `Helix.*` in TypeScript, it exists here with the same name and meaning. The web signature is canonical; differences are tracked in the [feature matrix](/docs/feature-matrix). ## Four languages, one API [#four-languages-one-api] The HELIX Unreal runtime ships with multiple scripting environments layered on native Unreal. All of them call the **same** SDK: See [Languages](/docs/native-sdk/languages) for setup and the same call written in each. ## Getting started [#getting-started] **Install the HELIX plugin** into your Unreal project (`HELIX` plugin, UE 5.x). It bundles the C++ SDK, the Blueprint nodes, and the PuerTS + UnLua runtimes. **Initialize** the subsystem with the world id and launch ticket the platform provides at boot: ```cpp FHelixInitParams Params; Params.WorldId = WorldId; Params.LaunchTicket = LaunchTicket; UHelix::Get()->Init(Params); ``` **Call the Platform API.** Auth, wallet, inventory, cloud save β€” identical to web. Pick your language in [Languages](/docs/native-sdk/languages). ## What's different from web [#whats-different-from-web] * **Networking.** Native worlds use Unreal's own dedicated-server networking, with HELIX [wrappers](/docs/native-sdk/networking) for replication β€” *not* the web multiplayer stack. * **Rendering & input** are the engine's. HELIX doesn't abstract the scene graph. * **The Platform API is unchanged.** That's the whole point. # Languages (/docs/native-sdk/languages) The HELIX Unreal runtime exposes the same Platform API to four environments. Choose by team and task β€” they're interoperable within a project, and they all hit the identical backend. ## One operation, four languages [#one-operation-four-languages] Granting nothing, just reading: fetch the player's profile and LIX balance. ```cpp UHelix* H = UHelix::Get(); H->Profile()->GetMyProfile(FOnProfile::CreateLambda([](const FHelixProfile& P) { UE_LOG(LogHelix, Display, TEXT("Hi %s"), *P.DisplayName); })); H->Wallet()->GetBalance(FOnBalance::CreateLambda([](const FHelixBalance& B) { UE_LOG(LogHelix, Display, TEXT("%lld LIX"), B.Lix); })); ``` ```text Event BeginPlay β†’ Helix Β· Profile Β· Get My Profile (latent) On Success β†’ Break Profile β†’ "Display Name" β†’ Print String β†’ Helix Β· Wallet Β· Get Balance (latent) On Success β†’ Break Balance β†’ "Lix" β†’ Print String ``` Every SDK call is a node. Async calls are **latent** nodes with `On Success` / `On Error` exec pins. ```ts // Identical to the Web SDK β€” this is the same TypeScript surface, inside Unreal. const me = await Helix.profile.getMyProfile(); const wallet = await Helix.wallet.getBalance(); console.log(`Hi ${me.displayName} β€” ${wallet.lix} LIX`); ``` ```lua local me = Helix.profile.getMyProfile() local wallet = Helix.wallet.getBalance() UE.Log(string.format("Hi %s - %d LIX", me.displayName, wallet.lix)) ``` ## Choosing a language [#choosing-a-language] | Language | Reach for it when | | ------------- | ----------------------------------------------------------------------------------------------------------- | | **C++** | You want the lowest-level binding, maximum performance, or you're already in C++ gameplay code. | | **Blueprint** | Designers/artists wire logic visually; rapid iteration without compiling. | | **PuerTS** | Your team knows TypeScript, or you're porting web world logic β€” the API shape is identical to `@helix/sdk`. | | **UnLua** | You prefer Lua, or you're bringing across Lua-based logic. | Because PuerTS runs the same TypeScript API as the [Web SDK](/docs/web-sdk), platform logic written for the web often moves to Unreal with little more than swapping the rendering/input layer. This is the 1:1 contract paying off. ## Async conventions [#async-conventions] * **C++** uses typed delegates (`FOnX::CreateLambda`). * **Blueprint** uses latent nodes with `On Success` / `On Error`. * **PuerTS** uses `async`/`await` and Promises, exactly like web. * **UnLua** calls are synchronous-looking (the binding yields under the hood). # Networking (Native) (/docs/native-sdk/networking) Native worlds **do not** use the web [multiplayer stack](/docs/web-sdk/multiplayer). They use Unreal's own networking β€” dedicated servers, actor replication, RPCs β€” and HELIX adds thin **wrappers** that make identity, tickets, and authoritative value checks ergonomic inside that model. The web runtime needs a networking layer because the browser has none; HELIX provides one. Unreal already has battle-tested networking, so forcing the web stack on it would be a downgrade. The [Platform API](/docs/platform-api) stays identical β€” only the transport differs. ## What HELIX wraps [#what-helix-wraps] * **Instance tickets β†’ connection.** Players arrive with a platform-issued Instance Ticket; the HELIX wrapper verifies it during Unreal's login/connection handshake so only authorized players join. * **Identity on the server.** Replicated player state is tied to the HELIX `Player` identity, not a raw connection β€” so `Helix.*` calls on the server know who they're for. * **Authoritative value checks.** Helpers to keep wallet/inventory/ownership decisions on the server, mirroring the [item execution tiers](/docs/platform-api/inventory#execution-tiers). ## The model [#the-model] Use Unreal replication as you normally would: * Server is authoritative; clients send input/RPCs. * Movement and cosmetic state replicate via Unreal's replication graph. * **Value-bearing actions** (grant item, charge LIX, award reward) go through the server-side [Platform API](/docs/platform-api), never a client RPC you trust blindly. ```cpp // server-side authoritative grant, same Platform API as web void AArenaGameMode::AwardWin(const FHelixPlayer& Player) { UHelix::Get()->Inventory()->ConsumeAndGrant(/* atomic, idempotent */); } ``` If it affects inventory, currency, or ownership, it's decided on the **server** via the Platform API β€” regardless of runtime. Unreal RPCs from clients are input, not authority. ## Scale [#scale] Native dedicated servers scale through Unreal's standard tooling and HELIX's hosting. Large-map, high-population worlds are a primary reason to choose Native over Web. # Reference (Native SDK) (/docs/native-sdk/reference) The C++ reference is generated from the HELIX Unreal plugin headers with **Doxygen** and converted to Markdown, refreshed when the plugin changes. Blueprint nodes, PuerTS, and UnLua bindings are generated 1:1 from the same C++ surface. The sample below shows the shape. ## `UHelixWallet` [#uhelixwallet] Concept: [LIX & Economy](/docs/platform-api/lix-economy). Web equivalent: [`Helix.wallet`](/docs/web-sdk/reference). ### `GetBalance` [#getbalance] ```cpp void GetBalance(const FOnHelixBalance& OnComplete); ``` Reads the current player's balances and invokes `OnComplete` with an `FHelixBalance`. **Bindings** | Runtime | Call | | --------- | ----------------------------------------------- | | C++ | `UHelix::Get()->Wallet()->GetBalance(Delegate)` | | Blueprint | `Helix Β· Wallet Β· Get Balance` (latent) | | PuerTS | `await Helix.wallet.getBalance()` | | UnLua | `Helix.wallet.getBalance()` | *** Because every binding is generated from the same C++ surface, the four languages can't drift from each other β€” and the whole surface is kept aligned with the canonical [Web SDK](/docs/web-sdk/reference) by the [feature matrix](/docs/feature-matrix) check in CI. # Apps & the manifest (/docs/phone/app-model) A Helix Phone app is a **web bundle** (HTML/CSS/JS) described by a **manifest**. The manifest is the contract between your app and the phone: it declares the app's identity, where its code lives, which [permissions](/docs/phone/permissions) it needs, and any in-app purchases. ## The manifest [#the-manifest] ```ts type PhoneAppManifest = { appId: PhoneManifestAppId; // unique id (built-in slug, or "vendor.app") name: string; // display name version?: string; // semver, e.g. "0.1.0" subtitle: string; // short tagline on the store card icon: string; // icon token (e.g. "solar:gallery-bold") accent: string; // brand hex, e.g. "#ff4f9a" entry?: string; // where the app loads from (see below) allowedOrigins?: string[]; // sandbox origin allow-list (e.g. ["self"]) permissions: PhonePermissionScope[]; // scopes the app may request contentRating: "Everyone" | "13+" | "16+" | "18+"; iap?: PhoneIapProduct[]; // optional in-app purchase catalog }; ``` A real third-party manifest (the bundled sandbox demo): ```ts { appId: "studio.example", name: "Studio Example", version: "0.1.0", subtitle: "Sandbox demo app", icon: "solar:code-square-bold", accent: "#f2f2f2", entry: "/phone-apps/studio-example/index.html", allowedOrigins: ["self"], permissions: [ "account.basic", "storage.app", "media.pick", "camera.capture", "wallet.read", "presence.world", "payments", "notifications.push", "messages.send_with_consent", "voice.calls", "social.discovery", "social.connections.read", ], contentRating: "16+", iap: [ { sku: "demo_boost_1", name: "Demo Boost", priceLix: 150, type: "consumable" }, { sku: "demo_theme_pack", name: "Demo Theme Pack", priceLix: 500, type: "non_consumable" }, ], } ``` ### App IDs [#app-ids] * **Built-in apps** use a bare slug: `helixgram`, `messages`, `camera`, … * **Third-party apps** use a namespaced `vendor.app` form (e.g. `studio.example`, `acme.notes`). The dot-separated shape keeps third-party IDs from ever colliding with first-party slugs. ### `entry` and the sandbox [#entry-and-the-sandbox] `entry` tells the phone where to load the app: | Value | Meaning | | ----------------------------- | -------------------------------------------------------------------------- | | `helix://apps/` | A **built-in** app rendered by the phone shell itself (native React view). | | `/phone-apps//index.html` | A bundle served by Helix. | | an `https://…` URL | A bundle hosted by the developer (must be listed in `allowedOrigins`). | Third-party apps load inside a **sandboxed iframe** and talk to the phone through the [bridge](/docs/phone/runtime-and-bridge). `allowedOrigins: ["self"]` restricts the bundle to its own origin. ### Content rating [#content-rating] `contentRating` is one of `Everyone` / `13+` / `16+` / `18+`. It's declared per app and surfaced in the store so players (and the platform's age gates) know what to expect. ## Built-in apps [#built-in-apps] The phone ships with these **first-party** apps, always installed: | App ID | Name | What it does | | ------------ | ---------- | ------------------------------------------- | | `helixgram` | Helixgram | Photo & video feed (Instagram-style) | | `h` | H | Public square / short posts (Twitter-style) | | `messages` | Messages | Universal DMs across worlds | | `phone` | Phone | Calls & the dialer | | `contacts` | Contacts | Friends, backed by the Helix social graph | | `wallet` | Wallet | LIX & Coins balances and history | | `album` | Album | Captured photos & videos | | `camera` | Camera | Capture from the current world | | `settings` | Settings | Phone preferences, permissions, appearance | | `calculator` | Calculator | Quick math | | `weather` | Weather | Forecast | | `browser` | Browser | In-phone web | | `notes` | Notes | Jot things down | | `app-store` | App Store | Discover & install apps | Built-in apps are special only in two ways: they can render as native shell views (`helix://`), and they're granted their declared permissions by default. Otherwise they call the **same Phone SDK** third-party apps use β€” so the SDK reference applies to both. Because the social feeds are first-party, the SDK exposes them directly as [`phone.helixgram`](/docs/phone/reference#feeds-helixgram--h) and [`phone.h`](/docs/phone/reference#feeds-helixgram--h) β€” any app with `social.discovery` can read and post to them. ## Install state [#install-state] A player's relationship to an app is an **install record**: `installed` or `uninstalled`. Built-in apps are pre-installed and can't be uninstalled. Third-party apps are installed from the [app store](/docs/phone/app-store) and can be removed (which also revokes their permissions and clears their notifications). # The App Store (/docs/phone/app-store) The **App Store** app is how players discover and install third-party phone apps. This page is the developer's side: how an app gets into the store, how installs work, and what the platform guarantees. ## What an entry is [#what-an-entry-is] A store entry is a [manifest](/docs/phone/app-model) plus a hosted bundle. The manifest provides the store card (name, subtitle, icon, accent, content rating), the permission list the player will see, and any [in-app purchases](/docs/phone/in-app-purchases). ## Install lifecycle [#install-lifecycle] **Discover.** The store lists every catalog app from `GET /api/v1/phone/apps`. Each card shows the declared permissions and content rating up front. **Install.** `POST /api/v1/phone/apps/:appId/install` creates an install record (`status: "installed"`) and grants only `account.basic`. The app now appears on the home screen. **Grant as needed.** The player grants additional [scopes](/docs/phone/permissions) on first use or in Settings. **Uninstall.** `DELETE /api/v1/phone/apps/:appId/install` removes the app, **revokes its permissions, and clears its notifications**. Built-in apps can't be uninstalled. ## REST surface (first-party / server) [#rest-surface-first-party--server] Apps themselves use the [SDK](/docs/phone/reference); these endpoints back the store and tooling: | Method | Path | Purpose | | -------- | --------------------------------------- | ------------------------------------------ | | `GET` | `/api/v1/phone/apps` | List the catalog (built-in + third-party). | | `GET` | `/api/v1/phone/apps/:appId` | A single manifest. | | `GET` | `/api/v1/phone/installs` | The player's installed apps. | | `POST` | `/api/v1/phone/apps/:appId/install` | Install. | | `DELETE` | `/api/v1/phone/apps/:appId/install` | Uninstall. | | `GET` | `/api/v1/phone/permissions` | Effective grants per app. | | `PUT` | `/api/v1/phone/apps/:appId/permissions` | Replace an app's grants. | | `GET` | `/api/v1/phone/permissions/audit` | Recent permission events. | ## Publishing today [#publishing-today] Today the catalog is **first-party curated** β€” an app's manifest is registered in the backend app registry (`phone-app-registry.ts`), and bundles are served from Helix. There is no self-serve upload or automated review queue yet. To ship a third-party app now, you work with the Helix team to register the manifest and host the bundle. ## Publishing, proposed [#publishing-proposed] The self-serve developer flow is designed and tracked on the [Proposed & in-progress](/docs/phone/proposed#app-store-self-serve-publishing) page: * A **developer portal** + `helix phone publish` CLI to submit a manifest and bundle. * A **review queue** with states (`draft β†’ in_review β†’ approved β†’ published`), content-rating checks, and permission justification. * **Versioned releases** and staged rollout. * **Revenue share** on IAP, mirroring the platform's creator economy. Until that lands, build and test against the local sandbox (the bundled `studio.example` app and your own bundle under `public/phone-apps/`), and use the manifest format above β€” it won't change when self-serve publishing ships. ## Distribution guarantees [#distribution-guarantees] Regardless of how an app is published, the platform guarantees: * **Sandboxing.** Third-party bundles run in an isolated iframe and reach the platform only through the permission-checked [bridge](/docs/phone/runtime-and-bridge). * **Consent.** No scope beyond `account.basic` is active without the player granting it, and any scope is revocable. * **Auditability.** Every permission use is recorded; abusive apps can be cut off per-scope. # Example app: World Postcard (/docs/phone/example-app) Let's build a real app end to end. **World Postcard** lets a player turn a moment from the world they're in into a shareable postcard. Along the way it uses **media**, **camera**, **presence**, **storage**, **payments (IAP)**, **notifications**, **Helixgram**, and the **UI** affordances β€” a broad cross-section of the [SDK](/docs/phone/reference). `media.pickFromAlbum` Β· `camera.mockCapture` Β· `presence.getCurrentWorld` Β· `storage` Β· `payments.purchase` Β· `helixgram.createPost` Β· `notifications.push` Β· `ui.shareSheet` Β· `ui.setBadge` Β· `lifecycle` ## 1. The manifest [#1-the-manifest] Declare exactly the scopes the features below use β€” nothing more. ```ts { appId: "acme.postcard", name: "World Postcard", version: "1.0.0", subtitle: "Turn this moment into a postcard", icon: "solar:postcard-bold", accent: "#f5d90a", entry: "/phone-apps/world-postcard/index.html", allowedOrigins: ["self"], permissions: [ "account.basic", "media.pick", "camera.capture", "presence.world", "storage.app", "payments", "notifications.push", "social.discovery" // to post to Helixgram ], contentRating: "Everyone", iap: [ { sku: "gold_frame", name: "Gold Frame", priceLix: 200, type: "non_consumable" } ] } ``` ## 2. Boot the SDK and restore a draft [#2-boot-the-sdk-and-restore-a-draft] ```ts title="app.ts" import { createHelixPhoneSdk } from "@helix/phone-sdk"; const phone = createHelixPhoneSdk({ appId: "acme.postcard" }); type Draft = { assetId?: string; caption: string; frame: "plain" | "gold" }; let draft: Draft = (await phone.storage.get("draft")) ?? { caption: "", frame: "plain", }; const me = await phone.account.getCurrentUser(); phone.ui.toast(`Hi ${me?.displayName ?? "traveler"} β€” make a postcard!`); // Persist the draft whenever the app is backgrounded. phone.lifecycle.on("suspend", () => phone.storage.set("draft", draft)); ``` ## 3. Get the photo β€” pick or capture [#3-get-the-photo--pick-or-capture] Let the player choose an existing album photo, or capture a new one from the world. ```ts async function choosePhoto() { const ok = await phone.permissions.request(["media.pick", "camera.capture"]); if (!ok.includes("media.pick")) { phone.ui.toast("Photo access is needed to make a postcard."); return; } const useCamera = await phone.ui.confirm({ title: "New photo?", body: "Capture from this world, or pick from your album.", confirmLabel: "Capture", cancelLabel: "Album", }); const asset = useCamera ? await phone.camera.mockCapture({ caption: "Postcard" }) // real capture proposed : await phone.media.pickFromAlbum(); if (asset) { draft.assetId = asset.id; render(asset); } } ``` ## 4. Stamp it with the current world [#4-stamp-it-with-the-current-world] Postcards say where you are. Read presence and use the world name as the location. ```ts async function currentLocation(): Promise { const w = await phone.presence.getCurrentWorld(); return w?.worldName ?? "Somewhere in Helix"; } ``` ## 5. Sell a premium frame (IAP) [#5-sell-a-premium-frame-iap] A "Gold Frame" is a one-time, non-consumable unlock. Check ownership, then offer it. ```ts async function applyGoldFrame() { const owned = await phone.payments.getEntitlements(); let hasGold = owned.some((e) => e.sku === "gold_frame"); if (!hasGold) { const buy = await phone.ui.confirm({ title: "Gold Frame β€” 200 LIX", body: "Unlock a premium gold frame for your postcards.", confirmLabel: "Buy", }); if (!buy) return; const ent = await phone.payments.purchase("gold_frame", { idempotencyKey: `gold_frame:${me?.userId}`, }); hasGold = Boolean(ent); } if (hasGold) { draft.frame = "gold"; render(); } } ``` ## 6. Post, notify, share [#6-post-notify-share] Compose the postcard, post it to **Helixgram**, push a **notification**, bump the app **badge**, and offer the **share sheet**. ```ts async function postPostcard() { if (!draft.assetId) return phone.ui.toast("Pick a photo first."); const asset = (await phone.media.listAlbum()).find((a) => a.id === draft.assetId); if (!asset) return; const location = await currentLocation(); // 1) Post to the Helixgram feed (needs social.discovery + media.pick) const post = await phone.helixgram.createPost({ asset, caption: draft.caption || "Wish you were here ✨", location, }); // 2) Notify the player it's live (deep-links back into this app) await phone.notifications.push({ title: "Postcard posted", body: `Your ${location} postcard is live on Helixgram.`, deepLink: "acme.postcard/posted", }); // 3) Badge the home-screen icon await phone.ui.setBadge(1); // 4) Offer to share it out await phone.ui.shareSheet({ items: [{ title: "My World Postcard", text: `A postcard from ${location}`, url: post?.id ? `https://helixgame.com/g/${post.id}` : undefined, }], }); // 5) Clear the draft draft = { caption: "", frame: "plain" }; await phone.storage.set("draft", draft); } ``` ## 7. Handle the deep link [#7-handle-the-deep-link] When the player taps the notification, the app re-opens with a launch context β€” route to the "posted" screen and clear the badge. ```ts const ctx = await phone.runtime.launchContext(); if (ctx?.source === "notification" && ctx.path.endsWith("/posted")) { showPostedScreen(); await phone.ui.setBadge(0); await phone.notifications.markAppRead(); } ``` ## What this demonstrates [#what-this-demonstrates] | Step | SDK surface | Scope | | ----------------- | --------------------------------------------------------- | ----------------------------------- | | Greet + toast | `account`, `ui.toast` | `account.basic` | | Pick / capture | `media.pickFromAlbum`, `camera.mockCapture`, `ui.confirm` | `media.pick`, `camera.capture` | | Location stamp | `presence.getCurrentWorld` | `presence.world` | | Draft persistence | `storage`, `lifecycle` | `storage.app` | | Premium frame | `payments` | `payments` | | Publish | `helixgram.createPost` | `social.discovery` (+ `media.pick`) | | Notify + badge | `notifications.push`, `ui.setBadge` | `notifications.push` | | Share | `ui.shareSheet` | β€” | | Deep link | `runtime.launchContext` | β€” | Drop the bundle under `public/phone-apps/world-postcard/` and register the manifest (see [the App Store](/docs/phone/app-store)). In a sandboxed bundle you don't pass a token β€” the SDK uses the [bridge](/docs/phone/runtime-and-bridge) automatically, so the exact code above runs unchanged. The bundled `studio.example` app is a minimal reference that exercises every bridge method. ## Next [#next] # In-app purchases (/docs/phone/in-app-purchases) Phone apps monetize with **in-app purchases (IAP)** priced in [LIX](/docs/platform-api/lix-economy). You declare a product catalog in your manifest; players buy with `phone.payments`. ## Declare products [#declare-products] ```ts // in the app manifest iap: [ { sku: "demo_boost_1", name: "Demo Boost", priceLix: 150, type: "consumable" }, { sku: "demo_theme_pack", name: "Demo Theme Pack", priceLix: 500, type: "non_consumable" }, { sku: "pro_monthly", name: "Pro", priceLix: 999, type: "subscription", period: "P1M" }, ] ``` | Type | Meaning | | ---------------- | ---------------------------------------------------------------------------------------------- | | `consumable` | Can be bought repeatedly (boosts, currency packs). Each purchase is a new entitlement. | | `non_consumable` | Bought once, owned forever (a theme, an unlock). Re-purchase returns the existing entitlement. | | `subscription` | Recurring access with an `expiresAt`. | ## Purchase [#purchase] The client **requests**; the server validates against the manifest, applies idempotency, and grants: ```ts const ent = await phone.payments.purchase("demo_theme_pack", { idempotencyKey: crypto.randomUUID(), }); if (ent) unlockTheme(ent.sku); ``` Networks retry. With a stable `idempotencyKey`, a retried purchase returns the *same* entitlement instead of charging twice. Generate it once per purchase attempt. ## Read what the player owns [#read-what-the-player-owns] ```ts const owned = await phone.payments.getEntitlements(); const hasPro = owned.some(e => e.sku === "pro_monthly" && !isExpired(e)); // on a fresh device: await phone.payments.restore(); ``` An entitlement: ```ts type PhoneIapEntitlement = { sku: string; productName: string; productType: "consumable" | "non_consumable" | "subscription"; priceLix: number; expiresAt: string | null; // future date for subscriptions mock: boolean; // true until LIX settlement ships }; ``` ## Settlement status [#settlement-status] Purchases currently **grant entitlements but do not debit LIX** β€” they're recorded with `mock: true` (`settlement: pending_lix_purchase_sheet`). When LIX settlement ships, the same `phone.payments.purchase(...)` call will present the platform purchase sheet and debit the player's wallet server-side; **your code won't change**. Build against this API now. Tracked on [Proposed & in-progress](/docs/phone/proposed#real-iap-settlement). ## Server authority [#server-authority] As everywhere in Helix, **value is server-authoritative**: the client can't grant itself an entitlement. The server checks the `payments` scope, verifies the `sku` exists in your manifest, applies idempotency, and (soon) charges LIX. Mirror the [platform economy rules](/docs/platform-api/lix-economy) β€” never trust a client-reported purchase. # The Helix Phone (/docs/phone) Every Helix player carries a **phone**. It isn't part of any one world β€” it travels with the player everywhere, the same way your real phone follows you from place to place. It has a home screen, a set of **built-in apps** (messages, camera, wallet, a photo feed, and more), and an **app store** where third-party developers publish their own apps built on the **Helix Phone SDK**. The Helix Phone is a tiny operating system that lives on top of the Helix platform. Apps are sandboxed web bundles; the **Phone SDK** is how they reach the player's identity, wallet, media, contacts, notifications, and the world they're currently in β€” always gated by permissions. ## Why a phone? [#why-a-phone] A world is a *place*. A phone is *personal* and *portable*. Putting social, payments, messaging, and capture behind a familiar phone metaphor means: * **Players** get one consistent place for their messages, photos, wallet, and friends β€” no matter which world they're in. * **Developers** get a ready-made distribution surface (the app store) and a high-level SDK, instead of rebuilding identity, payments, and media for every world. ## How it fits together [#how-it-fits-together]
The shell

The phone OS: home screen, app switcher, notifications, permissions, and the runtime that hosts apps.

Apps

Built-in (first-party) apps and third-party apps from the store. Each is a web bundle described by a manifest.

The Phone SDK

One object β€” createHelixPhoneSdk() β€” exposing notifications, media, wallet, contacts, messages, storage, and more.

## A taste of the SDK [#a-taste-of-the-sdk] Every app boots the SDK with its own `appId`, then calls high-level namespaces. Reading the player and posting a notification is just: ```ts import { createHelixPhoneSdk } from "@helix/phone-sdk"; const phone = createHelixPhoneSdk({ appId: "studio.example" }); const me = await phone.account.getCurrentUser(); await phone.notifications.push({ title: "Welcome", body: `Hi ${me?.displayName ?? "there"} πŸ‘‹`, }); ``` That call only works if the app **declared** `notifications.push` in its manifest and the player **granted** it β€” see [Permissions & consent](/docs/phone/permissions). ## Start here [#start-here] The Phone SDK is shipping. A handful of capabilities are wired end-to-end against **mocked** backends today (camera capture, voice calls, IAP settlement, live world presence) β€” these are flagged inline and tracked on the [Proposed & in-progress](/docs/phone/proposed) page. Everything else (messages, contacts, notifications, media/album, app storage, permissions, social feeds) is real. # Keeping these docs in sync (/docs/phone/keeping-docs-in-sync) The Phone SDK reference is meant to be **comprehensive**, which only works if it can't silently fall behind the code. This repo ships a small pipeline that compares the **live SDK surface** against a committed **snapshot of what the docs cover**, and flags drift automatically. ## How it works [#how-it-works] **Snapshot.** `content/docs/phone/_meta/sdk-surface.json` records every `namespace.method` the docs account for. It's generated from the real `lib/phone/sdk.ts`: ```bash node scripts/extract-phone-sdk-surface.mjs /lib/phone/sdk.ts --json \ > content/docs/phone/_meta/sdk-surface.json ``` The extractor reads the stable bridge method strings (`bridgeRequest("media.pickFromAlbum", …)`) plus a small allow-list of non-bridge methods β€” so any method an app can actually call is captured. **Detect.** `scripts/check-phone-sdk-sync.mjs` extracts the live surface and diffs it against the snapshot, reporting three buckets: * **βž• In the SDK, missing from docs** β†’ document it (and move it off [Proposed](/docs/phone/proposed)). * **βž– In docs, gone from the SDK** β†’ prune or mark removed. * **πŸ†• New namespaces** β†’ likely a whole new doc section. It exits non-zero on drift, so CI can act on it. ```bash PHONE_SDK_PATH=/lib/phone/sdk.ts node scripts/check-phone-sdk-sync.mjs # βœ… In sync β€” 74 methods across 18 namespaces. ``` **Act.** The [`phone-sdk-sync` workflow](https://github.com/hypersonic-laboratories/helix3-docs/blob/main/.github/workflows/phone-sdk-sync.yml) runs the checker when the monorepo signals a change, on a weekly safety-net schedule, and on demand. On drift it opens (or updates) a **`phone-sdk-drift` issue** listing exactly what changed. ## Wiring the trigger on the `helix3` branch [#wiring-the-trigger-on-the-helix3-branch] The docs repo can't see the private monorepo, so the monorepo **pushes** a signal when the SDK changes. Add this workflow to the Helix monorepo on the **`helix3`** branch β€” it fires only when `lib/phone/sdk.ts` (or the phone types) change, and sends the SDK file inline so no cross-repo token is needed: ```yaml title=".github/workflows/notify-docs-phone-sdk.yml (in the monorepo, on helix3)" name: Notify docs of Phone SDK change on: push: branches: [helix3] paths: - "lib/phone/sdk.ts" - "lib/phone/types.ts" jobs: notify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Dispatch to helix3-docs env: GH_TOKEN: ${{ secrets.DOCS_DISPATCH_TOKEN }} # PAT with repo scope on helix3-docs run: | SDK_B64=$(base64 -w0 lib/phone/sdk.ts) gh api repos/hypersonic-laboratories/helix3-docs/dispatches \ -f event_type=phone-sdk-updated \ -F client_payload[sdkBase64]="$SDK_B64" ``` That's the whole loop: **SDK changes on `helix3` β†’ docs repo checks drift β†’ drift opens an issue β†’ the reference and [Proposed](/docs/phone/proposed) page are reconciled β†’ snapshot regenerated.** If you'd rather not add the monorepo trigger yet, set a `PHONE_SDK_URL` repo secret (a raw URL to `lib/phone/sdk.ts` on `helix3`, with a token if private) and the weekly scheduled run will catch drift on its own. ## Scope & limits [#scope--limits] This pipeline tracks the **shape** of the SDK (namespaces and methods) β€” the thing most likely to drift and the thing the reference must mirror. It intentionally doesn't diff parameter types or prose; those are reconciled by a human (or an agent) when an issue is opened. It pairs with the main SDK's [auto-generated reference pipeline](/docs/cli#auto-generated-reference) β€” same philosophy, applied to the phone. # Permissions & consent (/docs/phone/permissions) Every sensitive Phone SDK call is gated by a **permission scope**. An app can only *use* a scope if it (1) **declared** it in its [manifest](/docs/phone/app-model) and (2) the player **granted** it. Calls without permission return empty/`null` rather than throwing β€” so your app degrades gracefully. ## The scopes [#the-scopes] ## How granting works [#how-granting-works] **Declare** the scopes in your manifest's `permissions` array. Requesting a scope you didn't declare is an error. **Default grants.** `account.basic` is always granted. Built-in apps get all their declared scopes by default. **Third-party apps start with only `account.basic`** β€” everything else needs the player's explicit consent. **Request at point of use.** Ask for a scope when the feature is first used, and check the result: ```ts const granted = await phone.permissions.request("media.pick"); if (!granted.includes("media.pick")) { phone.ui.toast("Media access is needed to attach a photo."); return; } ``` **Players manage grants** in Settings β†’ the app's permission list, and can revoke any scope except `account.basic` at any time. ## Checking permissions [#checking-permissions] ```ts await phone.permissions.list(); // β†’ granted scopes for this app await phone.permissions.has("wallet.read"); // β†’ boolean ``` Because ungranted calls return empty values, you can also just *try* and handle the empty result β€” but checking first lets you show better UI. ## Consent-gated messaging [#consent-gated-messaging] `messages.send_with_consent` is deliberately named: sending messages on a player's behalf is sensitive, so every send is **audited** and the player can revoke the scope to immediately cut an app off. Build messaging features assuming the player is watching who they message. ## The audit trail [#the-audit-trail] Every permission-relevant event is recorded β€” grants, revocations, denied calls, and calls to **undeclared** scopes: ```ts type PhonePermissionAudit = { appId: string; scope: PhonePermissionScope; outcome: | "granted" | "denied" | "undeclared" | "consent_granted" | "consent_revoked" | "consent_denied"; reason: string | null; createdAt: string; }; ``` Players can review this in Settings; it's also what powers abuse investigations. For developers, the takeaway is simple: **declare only what you use, and request it in context** β€” over-asking shows up in the audit log and erodes trust (and store standing). # Proposed & in-progress (/docs/phone/proposed) Everything on this page is **Proposed** or **In-progress**. APIs here may change name, shape, or be cut. The shipping surface is the [SDK reference](/docs/phone/reference); build production apps against that. This page exists so the picture is complete and so feedback lands before the API freezes. We split this into two buckets: * **In-progress** β€” the method exists in the SDK today but runs against a **mock** backend. * **Proposed** β€” a gap we found while documenting; a new surface we think the Phone should expose. *** ## In-progress (mocked today) [#in-progress-mocked-today] These ship in the SDK now and are marked mock in the reference. The method signatures are expected to stay; only the backend becomes real. ### Real camera capture [#real-camera-capture] `camera.mockCapture` produces a placeholder asset. **Proposed:** a real `camera.capture()` that grabs the actual framebuffer of the world the player is in (and, where available, device camera), writing a true photo/video into the album. ### Real voice calls [#real-voice-calls] `calls.*` simulate call state (`provider: "mock"`, `status: "pending_voice_api"`). **In-progress:** a real voice stack so `calls.start(contactId)` opens a live call, with ringing/connected/ended driven by the platform rather than `updateStatus`. ### Real IAP settlement [#real-iap-settlement] `payments.purchase` grants entitlements with `mock: true` and does **not** debit LIX yet. **In-progress:** the platform purchase sheet + server-side LIX settlement. The call signature is final β€” only the money movement is being added. ### Live world presence [#live-world-presence] `presence.getCurrentWorld` returns a fixed world. **In-progress:** real presence wired to the player's actual world/instance, plus a proposed `presence.subscribe(cb)` for live updates as they travel. ### Real social-discovery matching [#real-social-discovery-matching] `social.recordSwipe` always returns `matched: false`. **In-progress:** real mutual-match detection and a `social.getMatches()` surface. ### Placeholder wallet history [#placeholder-wallet-history] The Wallet app's transaction list is deterministic synthetic data until the economy ledger is wired in; shape is final. *** ## Proposed new surfaces [#proposed-new-surfaces] Gaps we found while documenting the SDK. Each would be additive. ### `phone.intents` β€” app-to-app deep links Proposed [#phoneintents--app-to-app-deep-links-proposed] Today apps deep-link into **themselves** via notifications. Proposed: a way to hand off to *another* app with the player's consent β€” e.g. a game opening the Camera, or any app opening a Helixgram composer. ```ts // Proposed await phone.intents.open("camera", { action: "capture", returnTo: "acme.postcard" }); const result = await phone.intents.request("media.pick"); // system picker, returns an AlbumAsset ``` ### `phone.location` β€” in-world position Proposed [#phonelocation--in-world-position-proposed] Presence gives the *world*; some apps want coordinates (a map, a geocache, a "near me"). Proposed: a coarse, permissioned `location.getPosition()` scoped to the current instance, behind a new `location.read` scope. ### `phone.ui.haptic` & `phone.ui.clipboard` Proposed [#phoneuihaptic--phoneuiclipboard-proposed] Small but expected affordances: ```ts // Proposed phone.ui.haptic("success"); // light | success | warning | error await phone.ui.clipboard.write("gg"); const text = await phone.ui.clipboard.read(); // consent-gated ``` ### Blob storage beyond 32 KB Proposed [#blob-storage-beyond-32-kb-proposed] `storage` is a 32 KB KV store. Proposed: `phone.files` for larger per-app blobs (saved games, exports) with signed URLs, distinct from the album. ### Server-initiated & scheduled push Proposed [#server-initiated--scheduled-push-proposed] `notifications.push` is client-initiated while the app runs. Proposed: a server endpoint so an app's backend can notify a player who isn't currently in the app, plus `notifications.schedule(at, …)` for reminders. ### Sensors & audio Proposed [#sensors--audio-proposed] Proposed read-only `phone.sensors` (accelerometer/orientation for motion-controlled apps) and a small `phone.audio` for SFX/looping clips, each behind its own scope. ### Contacts write Proposed [#contacts-write-proposed] `contacts.list()` is read-only. Proposed: app-created, app-scoped contacts (e.g. a guild app adding guildmates) β€” never touching the player's real social graph without consent. ### App Store self-serve publishing \[#app-store-self-serve-publishing] Proposed [#app-store-self-serve-publishing-app-store-self-serve-publishing-proposed] The catalog is first-party curated today. Proposed developer flow: * A **developer portal** + `helix phone publish` CLI to submit a manifest + bundle. * A **review queue**: `draft β†’ in_review β†’ approved β†’ published`, with content-rating and permission-justification checks. * **Versioned releases**, staged rollout, and **IAP revenue share** mirroring the [creator economy](/docs/platform-api/lix-economy). * **Widgets / live activities** on the home and lock screen as a later phase. *** ## Have an opinion? [#have-an-opinion] This list is where the Phone SDK is going β€” if something you need is missing or mis-shaped, that's the most useful feedback you can give before these freeze. The [sync pipeline](/docs/phone/keeping-docs-in-sync) will automatically move items off this page and into the [reference](/docs/phone/reference) as they ship. # Phone SDK reference (/docs/phone/reference) This is the **complete** surface of the Helix Phone SDK as it ships today (v0.1.0). Each method lists its required [permission scope](/docs/phone/permissions); calls without a granted scope return `null`/`[]` rather than throwing. Methods marked mock are wired against a mocked backend until the real integration lands β€” see [Proposed & in-progress](/docs/phone/proposed). ## Creating the SDK [#creating-the-sdk] ```ts import { createHelixPhoneSdk } from "@helix/phone-sdk"; const phone = createHelixPhoneSdk({ appId: "studio.example", // required β€” your manifest appId accessToken?: string, // first-party/server only; omit for sandboxed apps manifest?: PhoneAppManifest, bridgeNonce?: string, // usually read from ?helixBridgeNonce automatically }); ``` `createHelixPhoneSdk` returns an object with the namespaces below. It auto-selects the [bridge or direct transport](/docs/phone/runtime-and-bridge#two-execution-modes). *** ## runtime [#runtime] | Method | Returns | Notes | | ------------------------- | ------------------------------- | --------------------------------------------------------------------- | | `runtime.bootstrap()` | `PhoneBootstrapItem \| null` | Account, installed apps, permissions, launch context, runtime config. | | `runtime.manifest()` | `PhoneAppManifest \| null` | This app's manifest. | | `runtime.launchContext()` | `PhoneAppLaunchContext \| null` | How the app was opened (`web` / `notification` / `shell`). | ## account [#account] | Method | Returns | Scope | | -------------------------- | ---------------------- | -------------------------------- | | `account.getCurrentUser()` | `PhoneAccount \| null` | `account.basic` (always granted) | ```ts const me = await phone.account.getCurrentUser(); // { userId, username, displayName, avatarUrl, phoneNumber, bio } ``` ## permissions [#permissions] | Method | Returns | Notes | | ----------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------- | | `permissions.list()` | `PhonePermissionScope[]` | Scopes currently granted to this app. | | `permissions.has(scope)` | `boolean` | Check a single scope. | | `permissions.request(scopes)` | `PhonePermissionScope[]` | Request one scope or an array; returns granted scopes. Throws if a scope wasn't declared in the manifest. | ## storage [#storage] Per-app key–value storage. Keys match `^[a-zA-Z0-9._:-]{1,128}$`; values are JSON up to **32 KB**. Writes are atomic upserts. **Scope: `storage.app`.** | Method | Returns | | ---------------------------- | ---------------------------------- | | `storage.get(key)` | `T \| null` | | `storage.set(key, value)` | `PhoneAppStorageRecord \| null` | | `storage.delete(key)` | `void` | ```ts await phone.storage.set("draft", { caption: "gm", assetId }); const draft = await phone.storage.get<{ caption: string; assetId: string }>("draft"); ``` ## notifications [#notifications] Create and manage **this app's** notifications. Push is rate-limited to \~12/hour per app. **Scope: `notifications.push`.** | Method | Returns | Notes | | ------------------------------------------------ | --------------------------- | -------------------------------------------------------------- | | `notifications.list()` | `PhoneNotification[]` | This app's notifications. | | `notifications.push({ title, body, deepLink? })` | `PhoneNotification \| null` | `title` ≀120, `body` ≀500. `deepLink` opens the app at a path. | | `notifications.markRead(notificationId)` | `PhoneNotification \| null` | | | `notifications.markAppRead(appId?)` | `void` | Marks all of this app's notifications read. | ```ts await phone.notifications.push({ title: "Render ready", body: "Your postcard finished exporting.", deepLink: "studio.example/exports", }); ``` ## media [#media] The player's **album** (photos/videos). Upload accepts a `File`. **Scope: `media.pick`.** | Method | Returns | Notes | | ---------------------------------------------- | -------------------- | ------------------------------------------------------------- | | `media.listAlbum()` | `AlbumAsset[]` | All album assets, newest first. | | `media.pickFromAlbum()` | `AlbumAsset \| null` | The most recent asset (the picker UI returns the chosen one). | | `media.upload(file, { caption?, worldName? })` | `AlbumAsset \| null` | Upload an image/video into the album. | ```ts const photo = await phone.media.pickFromAlbum(); if (photo) attach(photo); ``` ## camera [#camera] Capture into the album. mock β€” `mockCapture` currently produces a placeholder asset (or copies `sourceAssetId`); real world/device capture is [proposed](/docs/phone/proposed). **Scope: `camera.capture`.** | Method | Returns | | --------------------------------------------------------------------- | -------------------- | | `camera.mockCapture({ sourceAssetId?, caption?, worldName?, type? })` | `AlbumAsset \| null` | ## wallet [#wallet] Read-only balances. **Scope: `wallet.read`.** | Method | Returns | | --------------------- | ---------------------------- | | `wallet.getBalance()` | `PhoneWalletBalance \| null` | ```ts const w = await phone.wallet.getBalance(); // w.lix.totalBalance, w.coins.balance β€” read-only ``` ## payments [#payments] In-app purchases priced in **LIX**, from the app's manifest `iap` catalog. Purchases are idempotent. mock β€” entitlements are granted but LIX isn't debited yet (see [In-app purchases](/docs/phone/in-app-purchases)). **Scope: `payments`.** | Method | Returns | Notes | | --------------------------------------------- | ----------------------------- | -------------------------------------------------------- | | `payments.getProducts()` | `PhoneIapProduct[]` | The app's product catalog. | | `payments.purchase(sku, { idempotencyKey? })` | `PhoneIapEntitlement \| null` | Pass an `idempotencyKey` so retries can't double-charge. | | `payments.getEntitlements()` | `PhoneIapEntitlement[]` | What the player owns. | | `payments.restore()` | `PhoneIapEntitlement[]` | Re-fetch entitlements (e.g. on a new device). | ```ts const ent = await phone.payments.purchase("demo_theme_pack", { idempotencyKey: crypto.randomUUID(), }); ``` ## contacts [#contacts] The player's contacts, backed by the Helix social graph. **Scope: `social.connections.read`.** | Method | Returns | | ----------------- | ---------------- | | `contacts.list()` | `PhoneAccount[]` | ## messages [#messages] Threads and sending, consent-gated. **Scope: `messages.send_with_consent`.** | Method | Returns | Notes | | ----------------------------------------------------- | ----------------------- | ---------------------------------------------- | | `messages.threads()` | `MessageThread[]` | Threads with unread counts. | | `messages.send({ participantId, body, attachment? })` | `Message \| null` | `body` ≀2000; `attachment` is an `AlbumAsset`. | | `messages.markRead(threadId)` | `MessageThread \| null` | | ## calls [#calls] Voice calls and call logs. mock β€” calls are simulated until the voice API lands. **Scope: `voice.calls`.** | Method | Returns | | ------------------------------------------------------ | ----------------- | | `calls.logs()` | `CallLog[]` | | `calls.mockCall(contactId)` | `CallLog \| null` | | `calls.mockIncoming(contactId)` | `CallLog \| null` | | `calls.updateStatus(callId, status, durationSeconds?)` | `CallLog \| null` | ## presence [#presence] The player's current world/instance. mock β€” returns a fixed world until live presence lands. **Scope: `presence.world`.** | Method | Returns | | ---------------------------- | ---------------------------- | | `presence.getCurrentWorld()` | `PhonePresenceWorld \| null` | ## social [#social] Opt-in **discovery** profiles and swiping (the dating/meeting primitive). Swipe matching is mock. **Scope: `social.discovery`.** | Method | Returns | Notes | | ---------------------------------------------------------------------------- | ----------------------------------------- | -------------------------- | | `social.getMyDiscoveryProfile()` | `PhoneSocialDiscoveryProfile \| null` | | | `social.updateDiscoveryProfile({ displayName?, bio?, avatarUrl?, active? })` | `PhoneSocialDiscoveryProfile \| null` | Create/update. | | `social.disableDiscoveryProfile()` | `void` | Sets the profile inactive. | | `social.getDiscoveryFeed({ limit? })` | `PhoneSocialDiscoveryProfile[]` | Opted-in candidates. | | `social.recordSwipe(candidateUserId, "like" \| "pass")` | `PhoneSocialDiscoverySwipeResult \| null` | | ## feeds: helixgram & h [#feeds-helixgram--h] Because Helixgram (photo/video) and H (short posts) are first-party, the SDK exposes them directly. Both require **`social.discovery`**; Helixgram `createPost`/`createStory` also require **`media.pick`**. ### phone.helixgram [#phonehelixgram] | Method | Returns | | -------------------------------------------------------------------------------- | -------------------------- | | `helixgram.feed(page?)` / `helixgram.followingFeed(page?)` | `HelixgramPost[]` | | `helixgram.userPosts(userId, page?)` | `HelixgramPost[]` | | `helixgram.stories(page?)` / `helixgram.userStories(userId, page?)` | `HelixgramStory[]` | | `helixgram.userSocial(userId)` | `HelixgramProfile \| null` | | `helixgram.searchUsers(query, limit?)` | `HelixgramProfile[]` | | `helixgram.follow(userId)` / `helixgram.unfollow(userId)` | result | | `helixgram.createPost({ asset, caption, location })` | `HelixgramPost \| null` | | `helixgram.createStory({ asset, caption, location })` | `HelixgramStory \| null` | | `helixgram.like(postId)` / `helixgram.unlike(postId)` | result | | `helixgram.comment(postId, body)` / `helixgram.deleteComment(postId, commentId)` | result | | `helixgram.deletePost(postId)` / `helixgram.deleteStory(storyId)` | `void` | | `helixgram.report({ subjectType, subjectId, reason, details? })` | result | `page` is `{ limit?, before? }` for cursor pagination. ### phone.h [#phoneh] | Method | Returns | | ---------------------------------------------------------- | ------------------ | | `h.feed(page?)` / `h.followingFeed(page?)` | `HPost[]` | | `h.trends({ limit? })` | `HTrend[]` | | `h.userPosts(userId, page?)` | `HPost[]` | | `h.replies(postId, page?)` | `HPost[]` | | `h.userSocial(userId)` | `HProfile \| null` | | `h.createPost(body)` / `h.reply(postId, body)` | `HPost \| null` | | `h.repost(postId)` / `h.like(postId)` / `h.unlike(postId)` | result | | `h.follow(userId)` / `h.unfollow(userId)` | result | | `h.report({ subjectType, subjectId, reason, details? })` | result | ## ui [#ui] Native phone UI affordances. No scope required (UI-only). | Method | Returns | Notes | | ----------------------------------------------------------- | -------------------- | ------------------------------------------------------ | | `ui.toast(body)` | `void` | Transient toast. | | `ui.confirm({ title, body?, confirmLabel?, cancelLabel? })` | `boolean` | Native confirm dialog. | | `ui.shareSheet({ items })` | `PhoneUiShareResult` | OS share sheet; `items` are `{ title?, text?, url? }`. | | `ui.setBadge(count)` | `void` | Set this app's home-screen badge count. | ## lifecycle [#lifecycle] | Method | Returns | Notes | | ---------------------------------- | ------------ | -------------------------------------------------------------------------------------------- | | `lifecycle.on(event, handler)` | `() => void` | Subscribe; returns an unsubscribe fn. Events: `foreground` / `suspend` / `resume` / `close`. | | `lifecycle.emit(event, snapshot?)` | `void` | Mostly internal. | See [Runtime & the bridge](/docs/phone/runtime-and-bridge#lifecycle-events) for the lifecycle model. *** This page covers the **entire** shipping SDK surface. A [drift-detection pipeline](/docs/phone/keeping-docs-in-sync) compares it against `lib/phone/sdk.ts` on the `helix3` branch and flags any namespace or method that lands or changes, so the reference can't silently fall behind the code. # Runtime & the bridge (/docs/phone/runtime-and-bridge) The Phone SDK is the same object everywhere, but **how** it talks to the phone depends on who's running it. Understanding this makes the whole SDK click. ## Two execution modes [#two-execution-modes] When you call `createHelixPhoneSdk({ appId })`, the SDK picks one of two transports automatically: Your app runs inside a **sandboxed iframe** in the phone shell. It has no access token. The SDK routes every call through the **bridge** β€” a `postMessage` channel to the phone shell, which performs the call on your behalf and enforces permissions. This is the mode third-party apps use. ```ts // No token β€” the SDK detects it's framed and uses the bridge. const phone = createHelixPhoneSdk({ appId: "studio.example" }); ``` A first-party context (or a server-side caller) passes an **access token**. The SDK calls the phone REST API (`/api/v1/phone/...`) directly with that token. ```ts const phone = createHelixPhoneSdk({ appId: "wallet", accessToken }); ``` The SDK decides with one rule: if it's running inside another window (`window.parent !== window`) and has **no** access token, it uses the bridge. Otherwise it calls the API directly. **Your app code is identical in both modes** β€” you never branch on it. ## The bridge protocol [#the-bridge-protocol] The bridge is a request/response protocol over `postMessage`, secured by a per-session **`helixBridgeNonce`** (passed to the iframe as a query param). The shape: ```ts // app β†’ shell window.parent.postMessage({ source: "helix-phone-app", appId, bridgeNonce, requestId, method: "notifications.push", // namespaced SDK method params: { title, body }, }, "*"); // shell β†’ app { source: "helix-phone", bridgeNonce, requestId, ok: true, result: { /* … */ }, // or: ok:false, error: "scope_not_granted" } ``` Requests time out after **8 seconds**. The nonce ensures one app can't impersonate another or read another app's responses. You normally never write this by hand β€” the SDK does it β€” but it's why every SDK method maps to a stable `method` string like `media.pickFromAlbum` or `storage.set`. Third-party code never gets the player's token, never sees other apps' data, and can't call the platform directly. The shell is the only thing holding credentials; it brokers every call and checks the app's granted [permissions](/docs/phone/permissions) first. That's what makes running untrusted apps safe. ## Bootstrap β€” one snapshot of everything [#bootstrap--one-snapshot-of-everything] On launch, an app reads a **bootstrap** snapshot that contains the account, installed apps, effective permissions, the launch context, and runtime config: ```ts const runtime = await phone.runtime.bootstrap(); // runtime.account β†’ the signed-in PhoneAccount // runtime.apps β†’ installed app manifests // runtime.permissions β†’ { [appId]: PhonePermissionScope[] } // runtime.launchContext β†’ how this launch happened ``` Convenience accessors wrap it: ```ts const me = await phone.account.getCurrentUser(); // runtime.account const ctx = await phone.runtime.launchContext(); // see below const manifest = await phone.runtime.manifest(); // this app's manifest ``` ### Runtime config [#runtime-config] The bootstrap's runtime block tells you the environment you're in: ### Launch context [#launch-context] How did the app open? Deep link from a notification, the home screen, or a web URL? ```ts type PhoneAppLaunchContext = { appId: string; url: string; path: string; source: "web" | "notification" | "shell"; receivedAt: string; }; ``` Use it to route β€” e.g. a notification with a `deepLink` lands the user on the right screen. ## Lifecycle events [#lifecycle-events] The phone is **foreground-only**: only the active app runs. Subscribe to transitions so you can pause timers, save drafts, or refresh on resume: ```ts const off = phone.lifecycle.on("suspend", () => saveDraft()); phone.lifecycle.on("resume", () => refresh()); phone.lifecycle.on("close", () => flush()); // later off(); // unsubscribe ``` | Event | Fires when | | ------------ | ----------------------------------------------------------- | | `foreground` | The app becomes the active app. | | `suspend` | The app is backgrounded (another app opened, phone closed). | | `resume` | The app returns to the foreground. | | `close` | The app is being torn down. | Because backgrounded apps are suspended, write important state to [`phone.storage`](/docs/phone/reference#storage) on `suspend`/`close` rather than relying on periodic saves. # Web SDK (/docs/web-sdk) The Web SDK is HELIX's **TypeScript** runtime and the **canonical API contract**. Worlds built here are playable instantly from a link on any device. Everything in the [Platform API](/docs/platform-api) is available, plus a full **multiplayer & networking** stack that is specific to the web runtime. The Web SDK's surface is frozen first; the [Native (Unreal) SDK](/docs/native-sdk) mirrors it 1:1. When you read a Platform API page, the Web (TS) tab is the canonical signature. ## Packages [#packages] | Package | Purpose | | ------------------- | ------------------------------------------------------------------ | | `@helix/sdk` | The client SDK β€” the `Helix` namespace used in browser/world code. | | `@helix/server-sdk` | The server SDK β€” `HelixInstance` and authoritative APIs. | | `@helix/cli` | Scaffold, run, validate, and deploy worlds. | ## Client [#client] ```ts import { Helix } from '@helix/sdk'; await Helix.init({ worldId: window.HELIX_WORLD_ID, launchTicket: window.HELIX_LAUNCH_TICKET, }); const me = await Helix.profile.getMyProfile(); ``` From here, the whole [Platform API](/docs/platform-api) is available on `Helix.*`, and the [networking API](/docs/web-sdk/multiplayer) on `Helix.network` / `Helix.instance`. ## Server [#server] Server logic extends `HelixInstance` and owns authoritative state. The runtime calls your lifecycle hooks: ```ts import { HelixInstance } from '@helix/server-sdk'; export default class MyInstance extends HelixInstance { async onCreate(ctx) {} async onPlayerJoin(player) {} async onPlayerLeave(player) {} async onEvent(player, event, payload) {} async onTick(dt) {} async onClose() {} } ``` ## Get started [#get-started] # Multiplayer & Networking (/docs/web-sdk/multiplayer) This networking stack is part of the **Web** runtime. Native (Unreal) worlds use the engine's own dedicated-server networking β€” see [Native networking](/docs/native-sdk/networking). The [Platform API](/docs/platform-api) (auth, LIX, storage, inventory) is shared; *this page is not.* HELIX ships its own real-time layer β€” built on **Colyseus** β€” so you never touch a raw socket or a room API directly. You work with **Instances**, **Events**, and the [`HelixInstance`](/docs/web-sdk#server) server class. The transport is an implementation detail: the public vocabulary stays World / Build / Instance / Player / Event, and you import `@helix/sdk` / `@helix/server-sdk` rather than `colyseus` directly. ## Instances [#instances] A player joins an **Instance** β€” a running session of your Build. Default capacity is 32 players (higher with review). ## Events [#events] Networking is message-based. Events flow clientβ†’server, serverβ†’client, and serverβ†’all. ### Client [#client] ```ts // listen Helix.network.on('wave', ({ from }) => showWave(from)); // send Helix.network.send('wave', {}); // request/response const { rank } = await Helix.network.request('get-rank', { boardId: 'arena' }); ``` ### Server [#server] ```ts import { HelixInstance } from '@helix/server-sdk'; export default class Arena extends HelixInstance { async onEvent(player, event, payload) { if (event === 'wave') { this.broadcast('wave', { from: player.id }); // relay to everyone } } } ``` ## The networking model [#the-networking-model] HELIX web multiplayer is a **hybrid social model**: * **Movement** is client-authoritative and server-relayed (default transform & broadcast \~10 Hz, 100–200 ms interpolation) β€” smooth, cheap, good enough for social worlds. * **Value** (wallet, inventory, rewards, moderation, membership) is **server-owned**. The server never trusts client events for these. Movement and cosmetic events can come from the client. Anything that grants currency, items, or rewards must be decided by your `HelixInstance` server code. This mirrors the [item execution tiers](/docs/platform-api/inventory#execution-tiers). A future authoritative mode (higher tick, fully server-owned state) is planned for competitive/ranked worlds. ## The realtime envelope [#the-realtime-envelope] Every message on the wire is a typed envelope β€” useful when debugging: ```ts { id: string; type: 'event' | 'request' | 'response' | 'error' | 'system'; event: string; payload: unknown; requestId?: string; senderId?: string; timestamp: number; } ``` ## Reference [#reference] * Web SDK β†’ [`Helix.network` / `Helix.instance` / `HelixInstance`](/docs/web-sdk/reference) * Guide β†’ [Build your first multiplayer world](/docs/guides/first-multiplayer-world) # Reference (Web SDK) (/docs/web-sdk/reference) This reference is generated from the `@helix/sdk` TypeScript source with **TypeDoc** and refreshed automatically when the SDK changes on `main`. See [the pipeline](/docs/cli#auto-generated-reference) for how it works. The page below is a **representative sample** of the generated output until the SDK source is wired in. ## `Helix.wallet` [#helixwallet] The player's economy surface. See the [LIX & Economy](/docs/platform-api/lix-economy) concept page for semantics. ### `getBalance()` [#getbalance] ```ts getBalance(): Promise ``` Returns the current player's balances. **Throws** β€” `HelixError('NOT_INITIALIZED')` if called before `Helix.init`. ### `onBalanceChanged(callback)` [#onbalancechangedcallback] ```ts onBalanceChanged(cb: (balance: Balance) => void): Unsubscribe ``` Subscribes to wallet changes; returns an unsubscribe function. Fires after a purchase settles server-side. *** ## `Helix.cloudSave` [#helixcloudsave] Durable per-world key–value storage. Concept: [Cloud Save](/docs/platform-api/cloud-save). ### `get(key)` [#gettkey] ```ts get(key: string): Promise ``` ### `set(key, value)` [#settkey-value] ```ts set(key: string, value: T): Promise ``` Writes durably; resolves once the write is committed. *** When the SDK source is connected, this section lists **every** namespace, class, method, parameter, return type, and error β€” auto-generated and always in sync with the shipped `@helix/sdk`. The hand-written [Platform API](/docs/platform-api) pages link here for exact signatures, and this reference links back to them for semantics. # Authentication & Identity (/docs/platform-api/authentication) Every player has **one HELIX identity**. There is no per-world signup: when a player opens a world, they're already signed in (or they play instantly as a guest and can upgrade later). Your world never handles passwords β€” it receives a verified identity from the platform. ## The launch flow [#the-launch-flow] When a player opens a world, HELIX issues a short-lived **Launch Ticket**. Your client hands it to the SDK at init; the SDK validates it and exposes the player. ```ts await Helix.init({ worldId: window.HELIX_WORLD_ID, launchTicket: window.HELIX_LAUNCH_TICKET, }); const user = await Helix.auth.getCurrentUser(); const me = await Helix.profile.getMyProfile(); ``` ```cpp FHelixInitParams Params; Params.WorldId = WorldId; Params.LaunchTicket = LaunchTicket; UHelix::Get()->Init(Params); UHelix::Get()->Auth()->GetCurrentUser(/* delegate */); ``` ```ts await Helix.init({ worldId, launchTicket }); const user = await Helix.auth.getCurrentUser(); ``` ```lua Helix.init({ worldId = WorldId, launchTicket = LaunchTicket }) local user = Helix.auth.getCurrentUser() ``` When a player later **joins a multiplayer instance**, the platform issues a separate **Instance Ticket** that the runtime verifies before admitting them. You don't manage tickets by hand β€” the SDK and CLI wire them up. ## `Helix.auth` [#helixauth] ## `Helix.profile` [#helixprofile] ## Guests and virality [#guests-and-virality] Guest play is a first-class path, not a fallback. A visitor who opens your link plays **immediately** as a guest; the platform surfaces a signup prompt at a natural moment (often with a small LIX incentive). Your code treats guests like any other player β€” they have an id, a profile, and a (stipend) wallet. ## Permissions [#permissions] A world declares the scopes it needs in its manifest β€” e.g. `auth.profile`, `instance.join`, `network.events`, `social.friends`. Players grant them on first launch. Request only what you use. HELIX is a 16+ platform with self-declared age gating and no forced ID verification, except privacy-preserving age assurance for 18+ worlds and KYC for real-money cashout. These are enforced by the platform β€” your world doesn't implement them. ## Reference [#reference] * Web SDK β†’ [`Helix.auth` / `Helix.profile`](/docs/web-sdk/reference) * REST β†’ `GET /v1/me` # Cloud Save (/docs/platform-api/cloud-save) `Helix.cloudSave` is HELIX's **durable** key–value store, scoped per world. It's the right place for anything that must outlive an instance: player progress, settings, world state. If you know Roblox, this is **DataStore**. Cloud Save is **durable and authoritative** β€” the source of truth. [Memory Store](/docs/platform-api/memory-store) is **volatile and fast** β€” caches, leaderboards, matchmaking β€” and always expires. When in doubt about where data belongs, ask: *"would losing this be a bug?"* If yes, it's Cloud Save. ## API [#api] ## Example [#example] ```ts type Progress = { level: number; coins: number }; // load on join const progress = (await Helix.cloudSave.get(`progress:${playerId}`)) ?? { level: 1, coins: 0, }; // save on change await Helix.cloudSave.set(`progress:${playerId}`, progress); ``` ```cpp UHelix::Get()->CloudSave()->Get( FString::Printf(TEXT("progress:%s"), *PlayerId), FOnJson::CreateLambda([](const FHelixJson& Value) { /* ... */ })); ``` ```lua local key = "progress:" .. playerId local progress = Helix.cloudSave.get(key) or { level = 1, coins = 0 } Helix.cloudSave.set(key, progress) ``` ## Semantics & guarantees [#semantics--guarantees] * **Per-world namespace.** Keys are isolated to your world; you can't read another world's data. * **Authoritative.** Writes are durable once `set` resolves. * **Write from the server for value-bearing data.** Persisting currency/inventory-adjacent state belongs to server-authoritative code β€” never let a client write its own balance. See the [golden rule](/docs/introduction/how-helix-works#server-authority--the-golden-rule). * **No direct database access.** Creator code reaches persistence only through this API, never a raw DB connection. Two instances of the same world can run at once. For counters or state edited by many players, prefer an atomic update pattern (read-modify-write guarded server-side) or use [Memory Store's `increment`](/docs/platform-api/memory-store) for hot, transient counters and flush to Cloud Save periodically. ## Reference [#reference] * Web SDK β†’ [`Helix.cloudSave`](/docs/web-sdk/reference) * REST β†’ `GET/PUT /v1/cloudsave/:worldId/:key` * Guide β†’ [Persist player data](/docs/guides/persist-player-data) # Platform API overview (/docs/platform-api) The Platform API is the part of HELIX that has nothing to do with rendering. It's the same on web and Native, with the same function names and the same guarantees. These pages describe the **semantics** β€” what each capability does, what the server guarantees, and what the error cases are. Runtime-specific signatures live in each runtime's reference, linked from every page. Each Platform API page is **language-neutral**. Code samples use the runtime tabs you've seen β€” pick your runtime once and it sticks. The Web (TS) signature is canonical; other runtimes mirror it. ## The surface [#the-surface] ## The `Helix` namespace [#the-helix-namespace] On every runtime, the platform is reached through a single `Helix` object. The full set: ```ts Helix.auth Helix.profile Helix.avatar Helix.world Helix.instance Helix.network Helix.social Helix.chat Helix.voice Helix.wallet Helix.inventory Helix.marketplace Helix.cloudSave Helix.memoryStore Helix.assets Helix.analytics Helix.moderation Helix.ui ``` Initialize it once at startup: ```ts await Helix.init({ worldId, launchTicket }); ``` ## Two rules that run through everything [#two-rules-that-run-through-everything] Anything affecting **inventory, currency, or ownership** is server-authoritative. Clients are trusted only for visuals and local gameplay. See the [Universal Item Contract](/docs/platform-api/inventory#execution-tiers). **Cloud Save** is durable and authoritative β€” the source of truth. **Memory Store** is volatile, fast, and always requires a TTL β€” never the source of truth. Don't reach for one when you mean the other. # Inventory & Items (/docs/platform-api/inventory) Items in HELIX are **universal**: an item a player owns exists across worlds and renders on every runtime (each item ships per-runtime renditions β€” e.g. a web GLB and an Unreal static mesh). `Helix.inventory` is how a world reads and uses what a player owns. ## Reading inventory [#reading-inventory] ```ts if (await Helix.inventory.hasItem('vip_hat_001')) { await Helix.inventory.equipItem('vip_hat_001'); } ``` ## Execution tiers [#execution-tiers] The most important thing to understand about items is **who is allowed to run their logic**. Every item declares a tier: **If it affects inventory, currency, or ownership, the server is authoritative. If it only affects visuals or local gameplay, the client can be trusted.** Tier 3 logic runs on the server, full stop. ## Consuming an item (Tier 3) [#consuming-an-item-tier-3] Consumption is server-authoritative and **idempotent** β€” a retried network call can't double-spend: ```ts // server-side (HelixServer / HelixInstance context) const result = await Helix.inventory.consumeItem({ itemId: 'health_potion_001', quantity: 1, idempotencyKey: requestId, }); // result.mintResults describes any items/currency atomically granted in return ``` ## Item lifecycle hooks [#item-lifecycle-hooks] Interactive items implement lifecycle hooks the runtime calls for you: ```ts onEquip, onUnequip, onPlace, onRemove, onInteract, onUpdate, onStateChanged ``` ## Reference [#reference] * Web SDK β†’ [`Helix.inventory`](/docs/web-sdk/reference) * Server authority β†’ [How HELIX works](/docs/introduction/how-helix-works#server-authority--the-golden-rule) * Economy β†’ [LIX & Economy](/docs/platform-api/lix-economy) # LIX & Economy (/docs/platform-api/lix-economy) HELIX has a real, platform-wide economy. Two currencies: * **Coins** β€” soft currency, earned and spent across worlds. * **LIX** β€” hard currency, bought with real money. **100 LIX = $1.** LIX pays for premium items, collectibles, platform services, and in-world purchases. Creators earn LIX and can cash it out; stipend LIX is spend-only. Every LIX-affecting action is settled on the server. Clients **request** purchases; they never grant currency or items. All in-world purchase charges deduct LIX server-side. Treat any client-reported balance or grant as untrusted. ## The wallet [#the-wallet] `Helix.wallet` reads balances and initiates purchases. ```ts const { lix, coins } = await Helix.wallet.getBalance(); ``` ## In-world purchases (IWP) [#in-world-purchases-iwp] To sell something for LIX, the **client requests** and the **server authorizes and settles**. The SDK gives you an idempotency key so retries can't double-charge. ```ts // client: request a purchase const result = await Helix.marketplace.purchaseItem('premium_sword_001'); if (result.status === 'completed') { // item is now in the player's inventory, charged server-side } ``` ```lua local result = Helix.marketplace.purchaseItem("premium_sword_001") if result.status == "completed" then -- granted + charged server-side end ``` For full control (consumables, custom pricing) the server validates and calls the authoritative grant/charge APIs β€” see the [Inventory](/docs/platform-api/inventory) execution tiers and the [Sell an item for LIX](/docs/guides/sell-item-for-lix) guide. ## The marketplace [#the-marketplace] `Helix.marketplace` lists items, collectibles, and listings, and initiates purchases. Rare collectibles trade **peer-to-peer** for LIX (redistribution, never minted), which is the core anti-inflation mechanism β€” low drop rates protect LIX value. ## Fees & payouts (platform policy) [#fees--payouts-platform-policy] * Platform commission β‰ˆ **25%** (creators keep \~75%), down to \~10% for top tiers. * Resale/trade fee β‰ˆ **10%**, down to \~3% by tier. * Creator earnings are cashable (with KYC); stipend LIX is not. These are platform-enforced β€” you don't implement them. Payment processors and ledger internals are never part of the public SDK. You call `Helix.wallet` and `Helix.marketplace`; HELIX handles settlement. ## Reference [#reference] * Web SDK β†’ [`Helix.wallet` / `Helix.marketplace`](/docs/web-sdk/reference) * Guide β†’ [Sell an item for LIX](/docs/guides/sell-item-for-lix) # Memory Store (/docs/platform-api/memory-store) `Helix.memoryStore` is HELIX's **volatile** store: fast, shared across every running instance of your world, and **always expiring**. It's for live state that's cheap to lose. If you know Roblox, this is **MemoryStore**. Every Memory Store entry **requires a TTL** and may vanish when it expires or under pressure. It has **no economic authority** β€” it cannot grant items or LIX. For anything that must survive, write to [Cloud Save](/docs/platform-api/cloud-save). ## API [#api] ## Live leaderboard example [#live-leaderboard-example] ```ts // record a score (atomic, ranked, shared across instances) const board = Helix.memoryStore.sortedMap('round-scores'); await board.set(playerId, score, 300); // 5-minute TTL // read the top 10 const top = await board.getRange({ limit: 10, descending: true }); ``` ## Hot counter example [#hot-counter-example] ```ts // count concurrent players in a zone without hammering Cloud Save const inZone = await Helix.memoryStore.increment(`zone:${zoneId}`, 1, 60); ``` ## When to use which [#when-to-use-which] | Need | Use | | -------------------------------------- | ------------------------------------------- | | Player progress, settings, owned state | [Cloud Save](/docs/platform-api/cloud-save) | | Live leaderboard for a round | Memory Store `sortedMap` | | Matchmaking / cross-instance handoff | Memory Store `queue` | | Hot counter (players in a zone) | Memory Store `increment` | | Anything you'd be upset to lose | [Cloud Save](/docs/platform-api/cloud-save) | ## Reference [#reference] * Web SDK β†’ [`Helix.memoryStore`](/docs/web-sdk/reference) * REST β†’ `/v1/memorystore/:worldId/...` # Social & Presence (/docs/platform-api/social) `Helix.social` exposes the player's social graph: who their friends are, who's online, and how to pull them into your world. Because identity is platform-wide, friendships and presence work across every world automatically. ## API [#api] ## Invite a friend into your world [#invite-a-friend-into-your-world] ```ts const friends = await Helix.social.getFriends(); const online = []; for (const f of friends) { const p = await Helix.social.getPresence(f.id); if (p.online) online.push(f); } // pull an online friend into this instance if (online[0]) await Helix.social.inviteToInstance(online[0].id); ``` ## Presence [#presence] Presence tells you whether a friend is online and, when permitted, what world they're in β€” the basis for "join friend" buttons and for surfacing active sessions. Respect the player's privacy settings; the platform enforces visibility. Real-time chat is `Helix.chat`; spatial/proximity voice is `Helix.voice` (see the [Add proximity voice](/docs/guides) guides). Friend discovery can optionally link external accounts (e.g. Discord) through official SDKs β€” never by scraping. ## Reference [#reference] * Web SDK β†’ [`Helix.social`](/docs/web-sdk/reference)