Local Build Box (self-hosted EAS builds on lbb)

Self-hosted mobile builds on the lbb Mac Studio via eas build –local — access, toolchain, build flow, and cost. Replaces EAS cloud builds at zero Expo cost.

Local Build Box (lbb)

Mobile builds are moving off EAS cloud to a self-hosted Mac Studio — lbb.thephenom.app (Logan’s machine) — using eas build --local. This reuses the existing eas.json profiles, runs entirely on our own hardware, and costs nothing on Expo.

Why

  • Free / off-quota. eas build --local runs the build on the box, not Expo’s servers. Verified empirically: a full local Android build (which even fetches the keystore from EAS) creates no build record and does not decrement the EAS build quota. Only cloud builds are metered. (Expo’s docs don’t state this explicitly; the account usage proves it.)
  • No upload limits. Because nothing is uploaded, the EAS Build 2 GB project-archive limit — and the workarounds for the 6 GB c2pa Rust target/ — simply don’t apply.
  • Fast. The box is a Mac Studio M4 Max, 16-core, 64 GB RAM, ~2.3 TB free (macOS 26). It’s also the right host for iOS (native xcodebuild → TestFlight).

Access

The developer build flow does not start with a direct SSH login from your laptop. It goes through Phenom’s C.O.D.E environment:

  1. Open code.thephenom.app in a browser and sign in via Cognito. This lands you in your own persistent browser-based VS Code (“box”), already running on the same Mac Studio that hosts the build toolchain below. See the C.O.D.E runbook for the login flow and environment details — this doc doesn’t duplicate them.
  2. From the integrated terminal inside that box, hop to the bare host over the internal network:
    ssh builder@host.orb.internal
    
    This lands you in the shared builder account. Key-based access is set up per-developer by appending your public key to /Users/builder/.ssh/authorized_keys (needs sudo, or the builder password) — ask in #dev if you don’t have access yet.
  3. Run the build commands documented below (expo prebuild, ./gradlew bundleRelease, …) from there, under builder, against /Users/builder/PhenomApp.

Accounts on the machine: personal logins, plus a dedicated builder account for CI/build work — the one used for the build flow above.

Toolchain

The Android toolchain is installed self-contained under /Users/Shared (not via Homebrew — /opt/homebrew is owned by another user, so brew is off-limits). Shared location so any build account can use it:

Component Location / version
JDK 17 (Temurin) → /Users/Shared/jdk-17/Contents/Home
Android SDK /Users/Shared/android-sdk
— platforms android-34, android-35
— build-tools 35.0.0
— NDK 27.1.12297006
— cmake 3.22.1
— platform-tools adb
Node / bun / eas-cli system-wide (node, bun, eas-cli)

Build accounts set in their shell profile (~/.zprofile):

export JAVA_HOME=/Users/Shared/jdk-17/Contents/Home
export ANDROID_HOME=/Users/Shared/android-sdk
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export PATH="$ANDROID_HOME/platform-tools:$JAVA_HOME/bin:$PATH"

Building

From an app checkout on the box, with an Expo login (eas login, or an EXPO_TOKEN robot token — preferred for the shared builder account):

Use the prebuild + Gradle route below, not eas build --local. As of 2026-08-06 the latter cannot build this app at all, and it leaks the production signing key on every run. Both problems are described under Why not eas build --local.

cd ~/PhenomApp/PhenomApp

# 1. generate the native project in place (never relocate it — see below)
bunx expo prebuild --platform android --clean --no-install

# 2. Sentry source-map upload needs an auth token the box does not have.
#    eas.json's play-internal profile sets these; raw Gradle does not inherit them.
export SENTRY_DISABLE_AUTO_UPLOAD=true
export SENTRY_ALLOW_FAILURE=true

# 3. build the signed AAB
cd android && ./gradlew bundleRelease --no-daemon

# output: android/app/build/outputs/bundle/release/app-release.aab

Prebuild writes the release signingConfig straight from credentials.json, so the AAB comes out signed with the production upload key (D1:E5:F1:04:…, CN=Project Phenom) with no extra wiring. Verify before uploading:

keytool -printcert -jarfile app-release.aab | grep SHA1

Why not eas build --local

It cannot build the app. eas build --local copies the project to a temp directory before building. Expo’s DOM-component transformer bakes an absolute path to PhenomGlobe.tsx (the "use dom" MapLibre component from #559), and that path does not survive relocation:

[EAGER_BUNDLE] Unable to resolve module …/Users/builder/PhenomApp/PhenomApp/app/components/phenom/map/PhenomGlobe.tsx

Not a CLI-version issue — reproduced identically on eas-cli 20.3.0 and 21.4.0, in a clean worktree and in the primary checkout. The bundle step itself is fine; running expo export:embed --eager --platform android in place bundles all 4009 modules plus the DOM component in under three seconds. Only the relocation breaks it.

It leaks the production signing key. eas build --local passes credentials to its build plugin as a single base64 argv blob containing the full keystore, keystorePassword and keyPassword. That means the production upload key is visible in ps for the duration of every build, and is written verbatim into any captured build log on failure. The Gradle route above keeps the keystore on disk and never puts it on a command line.

Gradle heap (required)

This app OOMs at the dex-merge (:app:mergeExtDexReleasejava.lang.OutOfMemoryError) on the default ~2 GB Gradle heap. The withGradleHeap config plugin sets org.gradle.jvmargs=-Xmx6144m so local and cloud builds succeed. (android/gradle.properties is regenerated at prebuild, so it must be a plugin, not an edited file.)

Distribution

A universal Android APK for this app is ~492 MB — over Signal’s 100 MB attachment cap, so you share a link/QR, not the file:

  • Host the APK (e.g., S3 + a CloudFront/presigned URL, or Firebase App Distribution) → send the download link via Signal, or open/scan on the device.
  • adb install app.apk if the device is tethered (USB or wireless adb).
  • Attestation caveat: a sideloaded APK still fails Play Integrity (C2PA signing enrollment returns 403 device attestation). For a build where signing must work end-to-end, deliver via the Play internal-testing track (the AAB), which is Play-recognized. See EAS Deployment.

Status

  • ✅ Toolchain (JDK 17 + Android SDK) installed and verified in /Users/Shared.
  • ✅ Off-quota / no-upload-limit mechanism confirmed: local builds on the box create no build record and don’t touch the EAS quota (validated on a developer Mac) — see Why.
  • eas build --local itself is unusable for this app — see Why not. The prebuild + Gradle route below is what actually ships (see next line).
  • builder-account wiring — shell env and app checkout in place; eas authenticated as smsmatt. Note the toolchain actually used is ~/dev-tools/jdk21 and ~/Library/Android/sdk, not the /Users/Shared paths above.
  • ✅ Signed AAB produced via prebuild + Gradle 2026-08-06 (production key verified).
  • ⏳ Artifact hosting (S3 link/QR) for the “build → share to device” flow — planned.