AgentStack
SKILL verified Apache-2.0 Self-run

Hz Store Pwa

skill-meta-quest-agentic-tools-hz-store-pwa · by meta-quest

Guides shipping a web app to the Meta Quest and Horizon OS Store as a PWA/TWA — both 2D windowed panels and immersive WebXR/VR. Covers building the web app (IWSDK for WebXR, any responsive PWA for 2D), Vercel deploy, web app manifest + icons, the WebXR-only auto-enter-session step, choosing 2D vs immersive mode in @meta-quest/bubblewrap-cli, keystore/Digital-Asset-Links, and ovr-platform-util Sto…

No reviews yet
0 installs
10 views
0.0% view→install

Install

$ agentstack add skill-meta-quest-agentic-tools-hz-store-pwa

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Hz Store Pwa? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Store PWA/TWA Skill

Guide the end-to-end process of wrapping a web app as a Meta Quest app and shipping it to the Meta Horizon Store. This skill covers both delivery modes — a 2D windowed panel and an immersive WebXR/VR experience — through the same pipeline: build the web app, deploy to Vercel, add a PWA manifest + icons, package as a signed Quest APK with @meta-quest/bubblewrap-cli, and upload with ovr-platform-util.

Commands use ` tokens (e.g. , , , `) — substitute your own values before running.

When to Use This Skill

Use this skill when you need to:

  • Ship a web app (2D or WebXR) to the Meta Horizon Store as a PWA/TWA
  • Decide whether an app should run as a 2D panel or an immersive WebXR session
  • Build a WebXR app with IWSDK and wire up auto-enter-session for the installed PWA
  • Deploy a PWA to Vercel and produce a valid, installable web app manifest + icons
  • Package a live PWA into a signed Quest APK with @meta-quest/bubblewrap-cli
  • Configure the signing keystore and Digital Asset Links so the TWA will launch
  • Upload a build to the Store with ovr-platform-util
  • Troubleshoot a 2D app stuck loading, an immersive app showing a URL bar, a TWA

that won't launch, or an upload that's blocked

For deeper IWSDK app-building guidance, see the hz-iwsdk-webxr skill. For the broader Store submission process (VRC compliance, store assets, review tracking), see the hz-store-submit skill.

Pipeline Overview

The full pipeline follows this order. The two mode-specific deltas are flagged; all other steps are identical for 2D and immersive.

0. Pick app mode        → 2D panel vs immersive WebXR (sets steps 1 + 4)
1. Build the web app    → IWSDK WebXR app (immersive) OR any responsive PWA (2D)
2. Deploy to Vercel     → public HTTPS origin = 
3. Manifest + icons     → installable web app manifest, PNG icons, live on 
4. Package as APK       → bubblewrap: keystore, twa-manifest, build, asset links
5. Upload to the Store  → ovr-platform-util upload-quest-build

Dependencies between steps matter — see [Order of Operations](#order-of-operations) at the end.

Step 0: Pick the App Mode First

The app mode is the single most important decision, chosen once. It changes exactly two things downstream:

  1. Whether the web app auto-enters a WebXR session on launch (immersive only).
  2. The horizonOSAppMode value in twa-manifest.json ("immersive" vs "2D").

| | 2D PWA | Immersive WebXR PWA | |---|---|---| | Runs as | windowed 2D panel on Horizon | enters a full VR/WebXR session | | Web app | any responsive PWA (IWSDK optional) | WebXR app (IWSDK is the easy path) | | Auto-enter requestSession | NO — do not add it (Step 1) | YES — built into the app (Step 1) | | horizonOSAppMode | "2D" (Step 4) | "immersive" (Step 4) |

A wrong horizonOSAppMode value is the classic failure mode: a 2D app set to immersive is stuck loading; an immersive app set to 2D shows a browser URL bar.

See [references/app-modes.md](references/app-modes.md) for the full decision guide.

Step 1: Build the Web App

Immersive WebXR app (IWSDK)

Scaffold with @iwsdk/create (the only supported scaffolder):

npx @iwsdk/create@latest  --yes --mode vr --no-metaspatial \
  --no-physics --no-locomotion --grabbing

Toggle --physics (Havok gravity/collisions), --locomotion (roam a large space), and --grabbing (hands/controllers pick objects up) to fit the app. For arcade-style apps prefer deterministic manual motion over physics.

Don't reinvent IWSDK app code. The template's bundled CLAUDE.md, .claude/skills/iwsdk-* skills, and the iwsdk-rag MCP are the source of truth for imports, ECS, XR input, physics, UI, and debugging. Query those rather than guessing.

Build auto-enter into the immersive app from the start. An installed immersive PWA opens with no 2D page, so the app itself must start the session on load (the app-icon tap is the user activation). Gate it on getDigitalGoodsService so it runs only in the installed PWA, never a browser tab:

const nav = navigator as Navigator & { xr?: { isSessionSupported?: (m:string)=>Promise } };
if ("getDigitalGoodsService" in window && nav.xr?.isSessionSupported) {
  nav.xr.isSessionSupported("immersive-vr")
    .then(s => { if (s) world.launchXR(); })   // IWSDK launchXR == requestSession + setup
    .catch(() => {});
}

getDigitalGoodsService is device-only — validate this path on the headset.

2D windowed app

Any responsive web app/PWA works — IWSDK is not required. It runs as a single- instance standalone panel with its own Library entry. Make sure it's a valid installable PWA (Step 3) and build/deploy it like any static/SPA site (Step 2). Do NOT add the auto-enter code above.

Full scaffolding flags, project layout, and the auto-enter rationale are in [references/app-modes.md](references/app-modes.md).

Step 2: Deploy to Vercel

The web app must be live on a public HTTPS origin before packaging — bubblewrap fetches the manifest and icons from it. Set base: "./" in your Vite config, then:

npx -y vercel@latest whoami
npx -y vercel@latest teams ls
npx -y vercel@latest deploy --prod --yes --scope 

Two URLs result:

  • Canonical alias https://.vercel.apppublic (200). Use this as

`` everywhere downstream.

  • Hashed per-deploy URL …-.vercel.app401 under deployment protection.

Not for sharing, not usable as ``.

Verify the root and manifest both return 200, and that the manifest is served as application/manifest+json:

curl -s -o /dev/null -w "%{http_code}\n" https:///manifest.webmanifest

See [references/vercel-deploy.md](references/vercel-deploy.md) for details and the redeploy-vs-rebuild rule.

Step 3: PWA Manifest + Icons

Both modes need a valid, installable manifest and PNG icons, live on ` before bubblewrap update runs. Place public/manifest.webmanifest`:

{ "name":"…","short_name":"…","description":"…","start_url":"/","scope":"/",
  "display":"standalone","orientation":"landscape",
  "background_color":"#06010f","theme_color":"#0a0418",
  "icons":[
    {"src":"/icons/icon-192.png","type":"image/png","sizes":"192x192","purpose":"any"},
    {"src":"/icons/icon-512.png","type":"image/png","sizes":"512x512","purpose":"any"},
    {"src":"/icons/icon-512-maskable.png","type":"image/png","sizes":"512x512","purpose":"maskable"}]}

Link it in index.html ` ( + `

  • `). For multi-origin 2D apps, add additionaltrustedorigins`

and host asset links on each origin.

There's no ImageMagick/PIL here — generate icons with sharp (npm i -D sharp) from an SVG. The maskable icon must be full-bleed and opaque (no transparency or rounded corners). Vite copies public/ into dist/.

See [references/manifest-and-icons.md](references/manifest-and-icons.md) for the icon script and the full manifest reference.

Step 4: Package as a Quest APK (bubblewrap)

bubblewrap wraps the live PWA into a signed Android APK (a Trusted Web Activity).

npm i -g @meta-quest/bubblewrap-cli   # bin: bubblewrap

Prereqs are pre-provisioned in ~/.bubblewrap (its own JDK 17 + Android SDK). Find the tools dynamically:

KT=$(find ~/.bubblewrap/jdk -path '*/bin/keytool' | head -1)
BT=$(ls -d ~/.bubblewrap/android_sdk/build-tools/* | sort -V | tail -1)

bubblewrap init uses an interactive inquirer wizard that needs a real TTY. A non-TTY caller (an agent driving Bash) can't answer it and there are no value flags to bypass it — use the scripted path below.

  1. Signing keystorepause and ask the developer which key to use before

building. The key is permanent: every future update must reuse it. Either reuse an existing keystore (ask for its path, alias, and passwords — required when updating a published app) or generate a new one outside the deployable web tree.

  1. twa-manifest.json (scripted path) — author it from the authoritative

TwaManifest schema. The critical field:

``json "horizonOSAppMode": "immersive" // ← "immersive" for WebXR | "2D" for a 2D panel app ``

A wrong value is the classic failure mode (Step 0). applicationId = numeric Horizon App ID ("0" builds & sideloads without IAP; set the real id before Store work).

  1. Buildupdate regenerates gradle and bumps version; passwords go via env

vars (no password CLI flags exist):

``bash cd export BUBBLEWRAP_KEYSTORE_PASSWORD= BUBBLEWRAP_KEY_PASSWORD= bubblewrap update && bubblewrap build # → app-release-signed.apk + app-release-bundle.aab "$BT/apksigner" verify --print-certs app-release-signed.apk | grep -i SHA-256 # must == keystore ``

  1. Digital Asset Links — a TWA "will not launch" if this fails. Host

public/.well-known/assetlinks.json on the same domain (and every trusted origin), with the package name and the colon-hex cert SHA-256. Redeploy, then curl https:///.well-known/assetlinks.json to confirm.

Security: the keystore and app secret NEVER go to the public host — verify with curl -o /dev/null -w "%{http_code}" https:///android.keystore (expect 404). Back up the keystore.

Full keystore handling, the complete twa-manifest.json template, build verification, and asset-link details are in [references/bubblewrap-packaging.md](references/bubblewrap-packaging.md).

Step 5: Upload to the Meta Horizon Store

hzdb / metavr are device-only and cannot upload. Use ovr-platform-util — the same command works for 2D and WebXR builds:

./ovr-platform-util upload-quest-build \
  --app-id  --app-secret  \
  --apk app-release-signed.apk \
  --channel ALPHA --age-group MIXED_AGES \
  --notes "…" --disable-progress-bar

Required: --app-id, --apk, --channel, --age-group (TEENS_AND_ADULTS | MIXED_AGES | CHILDREN), and --app-secret or --token. Channels: ALPHA/BETA/RC for testing, STORE for production. Auth is the app's App Secret (Dashboard → app → API tab) or a user token — ask the user, never invent it.

Likely first-time blocker: must first agree to our Developer Distribution Agreement — an org admin must sign it once at https://developer.oculus.com/manage/organizations//legal-documents/. Pause, ask the user, then retry the same command.

See [references/store-upload.md](references/store-upload.md) for tool download, auth, and the DDA blocker.

Order of Operations

  • Decide 2D vs immersive up front (Step 0) — it sets the auto-enter step (Step 1,

WebXR-only) and the horizonOSAppMode value (Step 4).

  • Manifest + icons must be LIVE before bubblewrap update — it fetches them from

``.

  • Asset links must be live before the TWA will launch — on every trusted origin.
  • Web-only fixes need only a Vercel redeploy — the installed TWA picks them up on

the next launch. Rebuild and re-upload the APK only for native changes (id, name, icon, version, app mode, packaging).

Gotchas

  • horizonOSAppMode mismatch is the #1 failure"2D" set to immersive hangs

on a loading screen; "immersive" set to 2D shows a URL bar. Fix the value and rebuild.

  • The signing key is permanent — every update must reuse the same keystore, alias,

and packageId. A lost keystore means a new app entry. Back it up, and keep it outside the deployable web tree.

  • bubblewrap init needs a TTY — agents must use the scripted path (hand-written

twa-manifest.json + update + build).

  • Auto-enter is immersive-only — never add the launchXR() snippet to a 2D app,

and always gate it on getDigitalGoodsService so it doesn't fire in a browser tab.

  • getDigitalGoodsService is device-only — auto-enter can't be validated in a

desktop browser or emulator; test on the headset.

  • Use the canonical Vercel alias — the hashed per-deploy URL returns 401 under

deployment protection and can't be used as ``.

  • The maskable icon must be full-bleed and opaque — transparency or rounded

corners produce visible artifacts after the platform applies its mask.

  • Asset links gate launch — a TWA "will not launch" until

/.well-known/assetlinks.json is live with a matching package name and cert SHA-256.

  • DDA blocks the first upload — only an org admin can sign it; it's the most

common first-time upload failure.

References

  • [App Modes: 2D vs Immersive](references/app-modes.md) — decision guide, IWSDK

scaffolding, project layout, and the auto-enter-session rationale.

  • [Vercel Deployment](references/vercel-deploy.md) — Vite config, deploy commands, the

canonical-vs-hashed URL distinction, and the redeploy-vs-rebuild rule.

  • [PWA Manifest + Icons](references/manifest-and-icons.md) — full manifest reference,

multi-origin setup, and the sharp icon-generation script.

  • [Bubblewrap Packaging](references/bubblewrap-packaging.md) — keystore handling,

twa-manifest.json template, build verification, asset links, and security.

  • [Store Upload](references/store-upload.md) — ovr-platform-util download, auth, the

upload command, and the Developer Distribution Agreement blocker.

  • [Troubleshooting](references/troubleshooting.md) — common failure modes across the

whole pipeline and how to fix them.

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.