Mobile (iOS)
Optional iOS development and dev-client workflow for local stacks.
Mobile app development (iOS)
This is optional. Most people can use the served web UI on mobile via Tailscale or point the Happier app to their self-hosted server (see App → Server URL).
Prereqs (one-time)
- Xcode installed
- CocoaPods installed (
brew install cocoapods)
Two supported modes
-
Shared dev-client app (recommended for development):
- Install one hstack dev-client app on your phone.
- Run any stack with
--mobile; scan the QR to open that stack inside the dev-client. - Per-stack auth/storage is isolated via
EXPO_PUBLIC_HAPPY_STORAGE_SCOPE(historical env var name; set automatically in stack mode).
-
Per-stack “release” app (recommended for demos / strict isolation):
- Install a separate iOS app per stack (unique bundle id + scheme).
- Each stack app is isolated by iOS app container (no token collisions).
Shared dev-client app (install once)
If you ran hstack setup --profile=dev, the setup wizard can optionally offer to install the dev-client for you.
Install the dedicated hstack dev-client app on your iPhone (USB).
This command runs a prebuild (generates ios/ + runs CocoaPods) and then installs a Debug build
without starting Metro:
hstack mobile-dev-client --installIf you want to ensure the dev-client is built from a specific stack’s active repo worktree (e.g. to include upstream changes that aren’t merged into your default checkout yet), run:
hstack stack mobile-dev-client <stack> --installOptional:
hstack mobile-dev-client --install --device="Your iPhone"
hstack mobile-dev-client --install --cleanThen run any stack with mobile enabled:
hstack tui stack dev <stack> --mobile
# or (without TUI):
hstack stack dev <stack> --mobile
# or:
hstack dev --mobileNotes:
- LAN requirement: for physical iPhones, Metro must be reachable over LAN.
- hstack defaults to
lanfor mobile, and will print a QR code + deep link. - For simulators you can usually use
localhost(seeHAPPIER_STACK_MOBILE_HOSTbelow).
- hstack defaults to
- If Expo is already running in web-only mode: re-run with
--restartand include--mobile.
Per-stack app install (isolated)
Install an isolated app for a specific stack (unique bundle id + scheme, Release config, no Metro):
hstack stack mobile:install <stack> --name="Happier (<stack>)"
hstack stack mobile:install <stack> --name="Happier PR 272" --device="Your iPhone"The chosen app name is persisted in the stack env so you can re-run installs without re-typing it.
Native iOS regeneration / “prebuild” (critical)
You’ll need to regenerate the iOS native project + Pods when:
- you pull changes that affect native deps / Expo config
apps/ui/ios/was deleted- you hit CocoaPods / deployment-target mismatches after a dependency bump
Run:
hstack mobile --prebuild
# (optional) fully regenerate ios/:
hstack mobile --prebuild --cleanWhat this does today:
- runs
expo prebuild --no-install(so we can patch before CocoaPods runs) - patches
ios/Podfile.properties.jsonto:- set
ios.deploymentTargetto16.0 - set
ios.buildReactNativeFromSourcetotrue
- set
- patches the generated Xcode project deployment target (where applicable)
- runs
pod install
Notes:
- You usually don’t need to run this manually because both:
hstack mobile-dev-client --installhstack stack mobile:install <stack>already include--prebuild.
- Legacy alias:
hstack mobile:prebuildexists (hidden), but preferhstack mobile --prebuild.
Manual hstack mobile usage (advanced)
If you want to work on the embedded Expo app directly (outside hstack dev --mobile), hstack mobile supports:
# Start Metro (keeps running):
hstack mobile --host=lan
# Build + install on iOS (and exit). If you omit --device, it will try to auto-pick a connected iPhone over USB:
hstack mobile --prebuild --run-ios --device="Your iPhone"
hstack mobile --prebuild --run-ios --configuration=Release --no-metroNotes / troubleshooting
-
QR opens the wrong app:
- The dev-client QR uses the
HAPPIER_STACK_DEV_CLIENT_SCHEME(default:hstack-dev). - Per-stack installs use a different per-stack scheme, so they should not intercept dev-client QR scans.
- The dev-client QR uses the
-
List connected devices (for
--device=):
hstack mobile:devices- Code signing weirdness on a real iPhone:
- hstack will try to “un-pin” signing fields in the generated
.pbxprojso Expo/Xcode can reconfigure signing (this avoids failures where automatic signing is disabled becauseDEVELOPMENT_TEAM/profiles were pinned). - If you want to manage signing manually, pass
--no-signing-fixtohstack mobile .../hstack stack mobile <stack> ....
- hstack will try to “un-pin” signing fields in the generated
Bake the default server URL into the app (optional)
If you want the built app to default to your hstack server URL, set this when building:
HAPPIER_STACK_SERVER_URL="https://<your-machine>.<tailnet>.ts.net" hstack mobile-dev-client --installNote: changing HAPPIER_STACK_SERVER_URL requires rebuilding/reinstalling the app you care about.
Important:
- For non-main stacks,
HAPPIER_STACK_SERVER_URLis only respected if it’s set in that stack’s env file (safety: we ignore “global” URLs for non-main stacks to avoid accidentally repointing other stacks).
Customizing the app identity (optional / advanced)
hstack uses these identities:
- Dev-client: defaults to
Happier Dev+ bundle iddev.happier.stack.dev.<user> - Per-stack release: defaults to
Happier (<stack>)+ bundle iddev.happier.stack.stack.<user>.<stack>
If you want to build/install manually (instead of mobile-dev-client / stack mobile:install), you can override:
- Bundle identifier (recommended for real iPhones):
- You may need this if the bundle id you’re using isn’t available/owned by your Apple team.
HAPPIER_STACK_IOS_BUNDLE_ID="com.yourname.hstack.dev" hstack mobile --prebuild --run-ios --no-metro- App name (what shows on the home screen):
HAPPIER_STACK_IOS_APP_NAME="hstack" hstack mobile --prebuild --run-ios --no-metroSuggested env (recommended)
Add these to your main stack env file (~/.happier/stacks/main/env) (or ~/.happier-stack/env.local for global overrides) so you don’t have to prefix every command:
# How the phone reaches Metro:
# - lan: recommended for real devices
# - localhost: OK for simulators
HAPPIER_STACK_MOBILE_HOST="lan"
# (optional) default scheme used in the dev-client QR / deep link
# (must match your installed dev-client app):
HAPPIER_STACK_DEV_CLIENT_SCHEME="hstack-dev"
# Default public server URL for the stack (baked into the Expo app config):
HAPPIER_STACK_SERVER_URL="https://<your-machine>.<tailnet>.ts.net"
# Optional: home screen name:
HAPPIER_STACK_IOS_APP_NAME="Happier"EAS builds per stack (production / App Store-like)
This section documents the end-to-end workflow for producing a fully working EAS Build (cloud) for a stack,
with a custom bundle id + app name (example: dev.happier.app / Happier), while keeping upstream defaults unchanged when env vars are unset.
Overview
To get a reliable EAS build for a stack-specific app identity, you generally need:
- a Happier monorepo worktree that contains the app config hooks (dynamic
app.config.js) - a stack pinned to that worktree (
hstack stack wt <stack> -- use ...) - stack env vars describing the app identity (bundle id, name, scheme, etc.)
- EAS environment variables configured on Expo (so the cloud builder sees the same values)
- an initial interactive credentials setup (iOS signing) before non-interactive builds work
- for identity changes: run prebuild so
ios/+android/native projects reflect the new id
1) Create (or select) a stack + pin a worktree
Example stack name: happier.
Create a monorepo worktree from your fork branch (example):
hstack wt new local/happier-eas --from=origin --base=origin/dev --usePin the happier stack to that worktree:
hstack stack wt happier -- use local/happier-eas2) Set the app identity in the stack env (local + build-time config)
These are evaluated by apps/ui/app.config.js when building:
hstack stack env happier set \
APP_ENV=production \
EXPO_APP_NAME="Happier" \
EXPO_APP_BUNDLE_ID="dev.happier.app" \
EXPO_APP_SCHEME="happier" \
EXPO_APP_SLUG="happier" \
EXPO_APP_OWNER="happier-dev"Notes:
APP_ENVdrives the built-in variant logic (development/preview/production).EXPO_APP_*overrides keep upstream defaults intact when unset.
3) Regenerate native projects (“prebuild”) when changing identity
When changing bundle IDs / schemes / config plugins, you should run a clean prebuild so native projects match the config:
hstack stack mobile happier --prebuild --platform=ios --clean --no-metro
# or:
hstack stack mobile happier --prebuild --platform=all --clean --no-metro4) Make sure the cloud builder sees your env vars (EAS Environment Variables)
Cloud builds do not automatically inherit your local shell env. You must configure EAS Environment Variables on Expo (project settings) and ensure your build profile selects an environment.
The EAS project env var workflow:
- Ensure
apps/ui/eas.jsonbuild profiles set"environment": "production"/"preview"/"development". - Sync the relevant EXPO config env vars into EAS for that environment (plain text or sensitive so
app.config.jscan evaluate them).
Recommended (stack-scoped sync helper):
hstack stack eas happier env:sync --environment production
# preview/development:
hstack stack eas happier env:sync --environment preview
hstack stack eas happier env:sync --environment development
# Safety knobs:
hstack stack eas happier env:sync --environment production --dry-run
hstack stack eas happier env:sync --environment production --hide-valuesNotes:
env:syncuses the current stack env values (for exampleEXPO_APP_*,EXPO_EAS_PROJECT_ID,EXPO_UPDATES_URL,EXPO_UPDATES_CHANNEL,EXPO_APP_LINK_HOST).env:syncdoes not syncEXPO_PUBLIC_*(those are bundle-inlined; useapps/ui/eas.jsonor EAS env vars UI as appropriate).
Manual EAS inspection (still useful for debugging a single variable):
hstack stack eas happier env:list --environment productionTip:
- If you see “No environment variables with visibility Plain text and Sensitive found…”, it means your EAS environment variables are missing (or set to
secret).
5) EAS project + account sanity checks
If you see permission errors, verify the Expo account:
hstack stack eas happier whoami
hstack stack eas happier loginIf the project is not initialized/linked under your account yet:
hstack stack eas happier project:init6) Build (iOS / Android)
First-time iOS credential setup usually requires interactive mode:
hstack stack eas happier ios --profile production --interactiveAfter credentials are set up, you can run non-interactive builds:
hstack stack eas happier ios --profile production --no-wait --non-interactive
hstack stack eas happier android --profile production --no-wait --non-interactiveCommon pitfalls
eas.json is not valid/"build.base" must be of type object:- your
eas.jsonis using an old schema; updatebuild.baseto an object and use"extends": "base".
- your
Credentials are not set up. Run this command again in interactive mode.:- run the same build with
--interactiveonce to configure iOS signing.
- run the same build with
- Cloud build doesn’t see stack env vars:
- set EAS environment variables (project settings) and make sure your profile has
"environment": "production".
- set EAS environment variables (project settings) and make sure your profile has