Theme
Last updated: 2026-10-09
Beta Testing is the only mode that behaves identically to a published app on a real user's phone, and the only mode that puts your app on the glasses menu. You pack the .ehpk exactly as you would for submission, promote it from Private to Beta, invite yourself (and optionally teammates) onto the app's tester list, and install it from the phone app's Beta tester section. The OS, the lifecycle, the lock-screen behavior, the system exit dialog - all of it matches what an end user (and a QA reviewer) sees.
If you're heading into review, this is the gate. Skip it and you'll fail.
Phone says the test version expired? Check both of these
The Even Realities App reports test version expired when a beta install is not backed by both of the following. The portal lets you do either one without the other, so getting one right and missing the other produces exactly that message.
- The build is tagged
Beta. In the Builds tab, click the build's greyPrivatetag, pick Beta, and confirm Promote to Beta. The tag itself is the control - the page has no separate distribute button, and the Beta build section stays hidden until something is promoted into it. - The tester is
Active. In the Testing group tab, invite the account that will install, and have that person accept the invitation email. The row's Status has to readActive; anInvitedtester can install nothing.
With both in place, the app appears under Beta tester in the phone app and on the glasses menu.
See Test for how Beta Testing compares to the simulator, local sideload, and private builds.
The flow
bash
# 1. Build and pack
npm run build
evenhub pack app.json dist -o myapp.ehpkThen in the dev portal:
Open hub.evenrealities.com/login and go to your project.
Testing group tab - Add user, enter your own account email, Send invite. Add teammates the same way if you want them on the same build. For more than a handful of testers use Import CSV instead of typing each address - see Moving a tester list in and out.
That tab is one flat list of testers for this app. Everyone on it gets the same beta build, and there is no way to create a second list, name it, or split testers into separate cohorts.
Open the invitation email and accept it. The row's Status goes from
InvitedtoActive. OnlyActivetesters can install a beta build.Builds tab - upload
myapp.ehpk. It lands under Private builds carrying a greyPrivatetag.Click that
Privatetag on the build's row. A menu opens withPublic/Beta/Private; pick Beta, then confirm with Promote to Beta.
Step 5 is the step people miss. The grey Private tag is also the control that changes it - clicking the tag is how a build goes from Private to Beta. The page carries no separate "distribute" or "publish to beta" button. The Beta build section renders only once something has been promoted into it, so an app that has never been promoted shows one Private builds section and nothing else.
Each app has one beta slot. Promoting a second version replaces the one in it.
Steps 2, 4 and 5 also run from a terminal:
bash
evenhub testers add teammate@example.com # step 2
evenhub upload myapp.ehpk -m "What changed" # step 4
evenhub promote 1.0.1 --to beta # step 5
evenhub ls <package_id> # CHANNEL should read betaStep 3 stays with the tester - accepting the invitation is a link in their email - and installing on the phone stays manual. See Testers.
On your phone (and any teammate's phone):
- Open the Even Realities App.
- Me → Beta tester lists the build.
- Tap Install.
From here on, the build behaves exactly as if it were published: it appears on the glasses menu, with full OS lifecycle and real backgrounding.
Moving a tester list in and out
The Testing group tab has Import CSV and Export CSV next to Add user. Since each app carries one flat tester list, CSV is how a roster moves between apps - export from one app, import into the next, instead of retyping every address.
evenhub testers add --from <file> reads the same file, header row and all.
File format
One column, UTF-8, row 1 is the literal header email, then one address per row:
csv
email
qa.lead@example.com
tester.one@example.com
tester.two@example.com- A file whose first row is anything other than
emailis refused before anything is sent. - At most 500 addresses per file. Split larger lists.
- Export writes exactly this shape, so an exported file imports straight into another app.
Import
Pick a file and the portal shows a breakdown before anything is committed:
| Line | Meaning |
|---|---|
| To invite | New addresses, plus testers whose invitation was rejected or expired |
| Skipped | Duplicated inside the file, or already Invited / Active on this app's list |
| Rejected rows | Rows that are not valid email addresses, listed (up to 50) so you can fix the file |
Confirm and every remaining address is invited in one request. Already invited and already active testers are never re-sent, so re-importing an exported file is safe: it invites nobody and disturbs nobody.
Two things can stop an import:
- Rejected rows - you may still commit the valid rows, or cancel and fix the file first.
- A tester who has hit the ten-invitation limit - the server refuses the whole batch and names the address. Nothing is imported. Remove that address from the file and import again.
Export
Export CSV downloads every tester on the list - not just the rows currently on screen - sorted alphabetically, with no status column. The file is named {date}_{app}_beta.csv, e.g. 2026-09-18_com.example.myapp_beta.csv.
What only Beta Testing validates
The things you can't test anywhere else:
The 5-minute locked-phone test
Open your app, put it in a steady state (something visible on glasses), lock the phone, wait 5 minutes, unlock. The app should still be where you left it. This is the exact test the QA reviewer runs. Private builds survive briefly but not 5 minutes; Local Testing dies the second the phone locks.
shutDownPageContainer(1) system exit dialog
The system exit-confirmation dialog only renders for a real install. Root-page double-tap must call bridge.shutDownPageContainer(1). Apps using 0 (immediate exit) or a custom in-app exit UI on the root page are auto-rejected at review.
Real permission denial paths
What does your app do when the user denies a permission? Beta Testing is where you actually exercise that branch. Local Testing skips some prompts; Private Testing fires them but the reviewer's denial path is a Beta-equivalent install.
What still requires care
- Console logs are visible in the phone app's Developer Mode console. Handy for debugging, but don't log secrets - that install path is reviewer-visible too.
- Crashes don't auto-report in beta yet. If your app vanishes, the console buffer is your only signal - check it immediately.
- Beta install is a real install.
localStorage, IndexedDB, everything persists exactly as it would for an end user. Reset between test rounds when you need a clean state.
The pre-submission checklist
Walk through each of these on the beta build before promoting to Submitted:
| # | Check | Pass criteria |
|---|---|---|
| 1 | 5-minute locked-phone test | Open the app, lock the phone for 5 minutes, unlock - state preserved, no spinner, no black screen. |
| 2 | Root double-tap fires the system exit dialog | Visible confirmation; WebView closes on confirm. |
| 3 | Permission denial paths handled | Deny each declared permission once - app degrades gracefully or surfaces a clear next-step message. |
| 4 | Re-launch a first-party app (Conversate, Navigate) after exit | Launches cleanly without restarting glasses. |
| 5 | No console errors at boot | Console buffer is clean immediately after launch. |
The full reviewer rubric: App Submission & QA Guidelines.
Common failure modes
| Symptom | Likely cause | Fix |
|---|---|---|
| Portal shows only Private builds, no way to distribute | The build was never promoted; the Beta build section is hidden until it is | Click the build's grey Private tag → Beta → Promote to Beta |
| Phone reports test version expired | One of the two gates is missing - nothing promoted to Beta, or the tester Status is still Invited | Promote the build to Beta, and accept the invitation so the status reads Active. Both, then reinstall |
| Tester accepted the invite but the build is gone again | Beta availability requires both an assigned beta build and Active membership | Check the beta build assignment and the tester’s membership. Adding an already accepted tester preserves membership. evenhub testers add skips exact Invited and Active email matches by default; --reinvite replaces pending invitation links and leaves Active members alone. Email matching preserves case. |
| App installs but never appears on the glasses menu | The install came from a Private build | The glasses menu lists Beta and published apps. Promote the build to Beta and reinstall |
| Install succeeds, app vanishes on lock | Backgrounding kills the WebView, no resume handler | Background & Lifecycle - persist eagerly to localStorage and rebuild on relaunch |
| Double-tap exits silently | shutDownPageContainer(0) or no exit handler | Use shutDownPageContainer(1) on the root page |
| Permission prompt copy is wrong | Edit app.json permission desc, repack, re-upload | Old beta still has old copy until reinstalled |
| Build updates don't propagate | Higher min_sdk_version than tester's firmware | Tester needs firmware update first, or you need to lower min_sdk_version |
When you're done
The .ehpk that passed every box above is the one you submit. Click that build's tag, pick Public, and confirm in Submit this build for review? - the tag then tracks the review itself: In Review, then Approved or Rejected, and Published once it is live. The reviewer installs from your beta build's lineage - if your beta passes the 5-minute test, the reviewer almost certainly will too.
→ Next: App Submission & QA Guidelines
Related
- Packaging & Deployment - the build path
- App Submission & QA Guidelines - the reviewer rubric
- Background & Lifecycle - what survives the 5-minute lock