3DX Phone

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.*.

index.html
<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.

CallPermissionNotes
phone.app.info()none{ id, version, sdk, permissions, phoneVersion, colorScheme }
phone.app.ready()noneCall once your first frame is painted.
phone.app.close()noneBack to the home screen.
phone.storage.get(key)noneThe stored JSON value, or null.
phone.storage.set(key, value)noneKeys 1 to 128 characters, values JSON.
phone.storage.remove(key)none
phone.storage.keys()nonestring[]
phone.notifications.post({ title, body })notificationsTitle up to 60, body up to 200 characters; one every 10 seconds.
phone.haptics.tap({ style })hapticsstyle: "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
server.js (Node 18+, from the kit's sdk/ folder)
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.