App reference.
Everything a 3DX Phone app can declare and call. New here? Start with the developer kit.
How an app runs
An app is a folder of web files: an HTML entry, your scripts and styles, images, fonts and sounds. On the phone it runs in its own window, separate from the phone and from every other app, and talks to the phone through a bridge. The SDK wraps that bridge as phone.*.
<script src="app-sdk.js" defer></script>
<script src="app.js" defer></script>With a bundler, import the ES module from the kit's sdk/ folder instead: import { phone, PhoneBridgeError } from "./sdk/index.js". The same app runs unchanged on the phone and in the kit's dev shell.
3dx-app.json
The App Store listing and what the app may use, next to your files.
id- Lowercase kebab-case, 2 to 64 characters. Yours once your first version is accepted; built-in app ids can't be used.
name- 2 to 40 characters, shown under the icon and in the App Store.
description- One sentence for the App Store, 8 to 140 characters.
longDescription- Optional, up to 2,000 characters.
whatsNew- Optional, up to 1,000 characters, shown with an update.
version- x.y.z. Each upload needs a higher version, except one that was rejected.
category- media, utility, social or lifestyle.
iconBackground- A CSS color or gradient for the icon tile.
iconColor- Optional CSS color.
ageRating- Optional, e.g. "17+". Without it the App Store shows 18+.
defaultInstalled- Always false for community apps.
removable- Always true for community apps.
permissions- What the app may use; see below. Leave it empty if it needs nothing.
entry- The HTML file to open, usually index.html.
minPhoneVersion- Optional x.y.z, the oldest phone version your app supports.
developer- Leave it out: the App Store shows your DX-ID avatar name.
Permissions
The phone shows these to the player before installing. Ask only for what the app uses.
- notifications
- Post notifications with phone.notifications.post.
- haptics
- Tap feedback with phone.haptics.tap.
- identity
- Ask for the player's DX-ID with phone.identity.get (Sign in with DX-ID).
- network:<host>
- Reach https://<host> and wss://<host>, e.g. network:api.my-app.example. One permission per host; no IP addresses or ports.
The phone API
Every call returns a promise. app.js loads as a classic script, so await goes inside an async function, as in the starter app.
| Call | Permission | Notes |
|---|---|---|
phone.app.info() | none | { id, version, sdk, permissions, phoneVersion, colorScheme } |
phone.app.ready() | none | Call once your first frame is painted. |
phone.app.close() | none | Back to the home screen. |
phone.storage.get(key) | none | The stored JSON value, or null. |
phone.storage.set(key, value) | none | Keys 1 to 128 characters, values JSON. |
phone.storage.remove(key) | none | |
phone.storage.keys() | none | string[] |
phone.notifications.post({ title, body }) | notifications | Title up to 60, body up to 200 characters; one every 10 seconds. |
phone.haptics.tap({ style }) | haptics | style: "light" or "medium". |
phone.identity.get({ scopes: ["profile"] }) | identity | { sub, avatarName, token }, or null when signed out or the player says no. |
Strings you pass are shown as plain text, never as HTML. There is no API for the Dynamic Island.
Errors
Calls reject with a PhoneBridgeError whose code is one of:
- permission-denied
- The call needs a permission your 3dx-app.json doesn't declare.
- invalid-payload
- A limit or a type is wrong.
- rate-limited
- More than one notification in 10 seconds.
- unknown-method
- The phone doesn't know the call (an older phone, or a typo).
- unavailable
- No phone around (outside the phone and the dev shell), or a feature that is off right now.
const { phone, PhoneBridgeError } = window.Phone3DX;
async function remind() {
try {
await phone.notifications.post({ title: "Hi", body: "Tonight at 22:00" });
} catch (err) {
if (err instanceof PhoneBridgeError && err.code === "rate-limited") retryLater();
}
}The sandbox
- Scripts
- Only from your own .js files. No inline <script>, no onclick="…", no eval or new Function.
- Data
- No localStorage, sessionStorage, IndexedDB or cookies: use phone.storage.
- Network
- Your own files, plus https and wss to the hosts in your network: permissions. Images, audio and fetch follow the same rule.
- Forms
- No form submission: handle the button's click and Enter yourself.
- Devices
- No camera, microphone, location, clipboard, USB or Bluetooth.
- Navigation
- The window stays on your app; no frames, workers or pop-ups.
Sign in with DX-ID
Apps that need accounts can use the player's DX-ID instead of their own logins. Apps never see passwords or DX-ID sessions. Declare identity, then ask:
async function signIn() {
const me = await phone.identity.get({ scopes: ["profile"] });
// null when the player is signed out or says no; otherwise { sub, avatarName, token }
return me;
}The first time, the phone asks the player and remembers the answer; players can take it back in Settings. Your app should still work without an account, or say why it needs one.
Check the token on your server
Anything in the app can be changed by whoever runs it, so send token to your own server (it needs a network: permission for that host) and verify it there before you trust it.
- format
- A JWT signed with Ed25519 (alg EdDSA, with a kid), valid for 5 minutes.
- iss
- https://sophey.vodka/api/dxid
- aud
- your app id
- sub
- the player's id for your app: stable in your app, different in every other app
- name
- the avatar name (scope profile)
- keys
- https://sophey.vodka/api/dxid/.well-known/jwks.json
import { verifyDXIDToken, DXIDTokenError } from "./sdk/server.js";
app.post("/session", async (req, res) => {
try {
const claims = await verifyDXIDToken(req.body.token, { appId: "my-app" });
res.json(await createSession({ id: claims.sub, name: claims.name }));
} catch (err) {
if (err instanceof DXIDTokenError) return res.status(401).json({ error: err.code });
throw err;
}
});Exchange the token for your own session right away instead of storing it. Not on Node? Any JWT library with JWKS and EdDSA support does the same checks. In the dev shell, identity.get returns a fake identity whose token never verifies.
Packages and limits
node 3dx-app.mjs pack turns your folder into a package: an index of every file with its hash, and the files. The App Store signs the index when your version is approved, and the phone checks the signature and every hash before it runs anything.
- Files per app
- 200
- Size per file
- 10 MB
- Size per app
- 25 MB
- Accepted uploads per day
- 10
- Versions in review per app
- 3
- Storage per app on the phone
- 1 MB of JSON
File types: html, js, mjs, css, json, svg, png, jpg, gif, webp, woff, woff2, mp3, ogg, wav. Paths can't contain .. or backslashes, and __3dx/ is reserved.
