Deploy the glasses app to Railway
Run the MentraOS AppServer (apps/glasses) on Railway so JQ works without your
laptop + ngrok. Mirrors the apps/backend Railway service — same monorepo
NIXPACKS pattern (apps/glasses/railway.json is already committed and correct).
The glasses reach this server through the MentraOS relay, so it must be a public, always-on URL — exactly what Railway gives you. After deploy you paste that URL into the Mentra dev console as the app's server URL (replacing ngrok).
1. Create the service
Dashboard (easiest):
- railway.app → your HiveJournal project → New →
GitHub Repo →
point-seven-studio/hivejournal-2026. - After it's created: service Settings →
- Root Directory: leave as the repo root (
/). The build/start commands inapps/glasses/railway.jsonalreadycd apps/glasses, matching how the backend service is set up — do NOT set root toapps/glassesor thecd apps/glassesin the commands double-nests. - Config file path:
apps/glasses/railway.json(so Railway uses the glasses build/start, not the backend's). - Networking → Generate Domain → copy the public URL (e.g.
https://hivejournal-glasses-production.up.railway.app).
- Root Directory: leave as the repo root (
CLI (if you prefer): railway login (interactive), then from the repo root
railway link (pick the project) → create a service → railway up. Set the
config path + domain in the dashboard as above.
2. Set environment variables
Service → Variables → add:
| Var | Value | Secret |
|---|---|---|
MENTRA_PACKAGE_NAME | fm.graphene.jq (your console package name) | no |
MENTRA_API_KEY | the key from console.mentra.glass | yes |
HIVEJOURNAL_API_URL | https://hivejournalbackend-production.up.railway.app | no |
HIVEJOURNAL_TOKEN | a durable device token (hjg_…) minted at /dashboard/glasses → "Connect this device" — never expires. (A browser JWT works for a local test but 401s within the hour — don't use it for a deploy.) | yes |
Leave PORT unset — Railway injects it and the app reads process.env.PORT.
Optional: SOPHIA_ENABLED / WATCH_ENABLED stay unset (off by default).
3. Point the Mentra console at the Railway URL
console.mentra.glass → your app → set the server URL (a.k.a. public URL / webhook URL) to the Railway domain from step 1. Confirm the Microphone AND Camera permissions are enabled (camera = the AisleAsk scan features).
4. Verify
- Railway Deploy logs show the build succeed and the process stay up (the
AppServer binds
PORT; no crash-loop).restartPolicyMaxRetries: 10in railway.json means a bad env crashes it 10× then stops — check logs if the service won't stay green. - From the MentraOS phone app, start the app and press the glasses
button → a
next_stepnudge in your voice. Then try the voice commands (seeapps/glasses/README.mdfor the full list). - Prereqs, or you'll hear a fallback line instead: a JQ voice clone
(
/dashboard/lovio/voice-setup) and an AisleAsk list (/dashboard/aisleask).
5. Redeploys
Railway auto-deploys on push to the tracked branch (default main). A glasses
code change → merge to main → Railway rebuilds. No manual step.
Notes
- Prototype auth is single-user — one
HIVEJOURNAL_TOKEN= one wearer. Multi-user account-linking (map the MentraOSuserId→ an HJ account) is a later phase; seeapps/glasses/README.md→ Auth. - The token is a bearer credential — Railway Variables only, never in the repo or the Mentra console. Rotate by replacing the variable value.
Gotchas (hit on the first real deploy, 2026-07-24)
Two Railway-side issues that made the first deploy fail even though the code + railway.json were correct:
- Railway Express builder ignores
railway.json. If the service has the "Try Railway Express (Early Preview)" feature-flag ON, Railway auto-detects the monorepo as "TypeScript · React", runsnpm run buildat the repo root (nobuildscript there) and never readsapps/glasses/railway.json→npm error Missing script: "build". Fix: Settings → Feature-flags → turn Railway Express OFF so it uses Nixpacks (which honorsrailway.json). - Node must be ≥ 20.19.
@mentra/sdkships ESM-syntaxdist/index.jsbut is mislabeledtype: null(CJS). Node only classifies it as ESM — makingimport { AppServer } from '@mentra/sdk'resolve — from 20.19.0 onward (unflagged module-syntax detection). On 20.18.x it crashes at runtime withSyntaxError: Named export 'AppServer' not found … is a CommonJS module(tsx/esbuild hides this in dev). The repo.nvmrcpins20.19so Railway matches. If you see that error, check the deploy log's Node version.
Runtime: tsx, not node dist (2026-07-24, the fix that stuck)
Config-based Node pins (.nvmrc, mise.toml) did not reliably move Railway's
mise off Node 20.18 on this service, so the @mentra/sdk ESM-named-import crash
kept recurring. The durable fix: run the app with tsx in production instead
of compiling to dist/ and running node dist/index.js. tsx's esbuild loader
does the CJS/ESM interop itself, so the mislabeled @mentra/sdk loads on any Node
version. Concretely: start = tsx src/index.ts, tsx moved to dependencies
(so it survives --omit=dev), build = tsc --noEmit (typecheck only — no dist),
and railway.json buildCommand = just npm install. Smoke-verified: the
AppServer boots and listens on the port.
MiniApp Type: Foreground (not Background) — 2026-07-24
Set the console MiniApp Type to Foreground. MentraOS routes the microphone / transcription stream to the foreground MiniApp, so a Background JQ connects and can speak but never receives the wearer's voice ("it's not listening — silence"). The physical buttons are also hardware-reserved on Mentra Live (left = battery, right = camera), so they never reach the app either. Voice is the only interaction, and it requires Foreground. Verified on-device: after flipping to Foreground, "add milk to my list" → "Added milk to your list."