setup-guides

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):

  1. railway.app → your HiveJournal project → NewGitHub Repopoint-seven-studio/hivejournal-2026.
  2. After it's created: service Settings
    • Root Directory: leave as the repo root (/). The build/start commands in apps/glasses/railway.json already cd apps/glasses, matching how the backend service is set up — do NOT set root to apps/glasses or the cd apps/glasses in the commands double-nests.
    • Config file path: apps/glasses/railway.json (so Railway uses the glasses build/start, not the backend's).
    • NetworkingGenerate Domain → copy the public URL (e.g. https://hivejournal-glasses-production.up.railway.app).

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:

VarValueSecret
MENTRA_PACKAGE_NAMEfm.graphene.jq (your console package name)no
MENTRA_API_KEYthe key from console.mentra.glassyes
HIVEJOURNAL_API_URLhttps://hivejournalbackend-production.up.railway.appno
HIVEJOURNAL_TOKENa 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: 10 in 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_step nudge in your voice. Then try the voice commands (see apps/glasses/README.md for 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 MentraOS userId → an HJ account) is a later phase; see apps/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:

  1. 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", runs npm run build at the repo root (no build script there) and never reads apps/glasses/railway.jsonnpm error Missing script: "build". Fix: Settings → Feature-flags → turn Railway Express OFF so it uses Nixpacks (which honors railway.json).
  2. Node must be ≥ 20.19. @mentra/sdk ships ESM-syntax dist/index.js but is mislabeled type: null (CJS). Node only classifies it as ESM — making import { AppServer } from '@mentra/sdk' resolve — from 20.19.0 onward (unflagged module-syntax detection). On 20.18.x it crashes at runtime with SyntaxError: Named export 'AppServer' not found … is a CommonJS module (tsx/esbuild hides this in dev). The repo .nvmrc pins 20.19 so 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."

GLASSES RAILWAY DEPLOY — Docs | HiveJournal