Happier Docs
hstack (local stack)

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 --install

If 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> --install

Optional:

hstack mobile-dev-client --install --device="Your iPhone"
hstack mobile-dev-client --install --clean

Then run any stack with mobile enabled:

hstack tui stack dev <stack> --mobile
# or (without TUI):
hstack stack dev <stack> --mobile
# or:
hstack dev --mobile

Notes:

  • LAN requirement: for physical iPhones, Metro must be reachable over LAN.
    • hstack defaults to lan for mobile, and will print a QR code + deep link.
    • For simulators you can usually use localhost (see HAPPIER_STACK_MOBILE_HOST below).
  • If Expo is already running in web-only mode: re-run with --restart and 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 --clean

What this does today:

  • runs expo prebuild --no-install (so we can patch before CocoaPods runs)
  • patches ios/Podfile.properties.json to:
    • set ios.deploymentTarget to 16.0
    • set ios.buildReactNativeFromSource to true
  • 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 --install
    • hstack stack mobile:install <stack> already include --prebuild.
  • Legacy alias: hstack mobile:prebuild exists (hidden), but prefer hstack 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-metro

Notes / 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.
  • 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 .pbxproj so Expo/Xcode can reconfigure signing (this avoids failures where automatic signing is disabled because DEVELOPMENT_TEAM/profiles were pinned).
    • If you want to manage signing manually, pass --no-signing-fix to hstack mobile ... / hstack stack mobile <stack> ....

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 --install

Note: changing HAPPIER_STACK_SERVER_URL requires rebuilding/reinstalling the app you care about.

Important:

  • For non-main stacks, HAPPIER_STACK_SERVER_URL is 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 id dev.happier.stack.dev.<user>
  • Per-stack release: defaults to Happier (<stack>) + bundle id dev.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-metro

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 --use

Pin the happier stack to that worktree:

hstack stack wt happier -- use local/happier-eas

2) 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_ENV drives 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-metro

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

  1. Ensure apps/ui/eas.json build profiles set "environment": "production" / "preview" / "development".
  2. Sync the relevant EXPO config env vars into EAS for that environment (plain text or sensitive so app.config.js can 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-values

Notes:

  • env:sync uses the current stack env values (for example EXPO_APP_*, EXPO_EAS_PROJECT_ID, EXPO_UPDATES_URL, EXPO_UPDATES_CHANNEL, EXPO_APP_LINK_HOST).
  • env:sync does not sync EXPO_PUBLIC_* (those are bundle-inlined; use apps/ui/eas.json or EAS env vars UI as appropriate).

Manual EAS inspection (still useful for debugging a single variable):

hstack stack eas happier env:list --environment production

Tip:

  • 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 login

If the project is not initialized/linked under your account yet:

hstack stack eas happier project:init

6) Build (iOS / Android)

First-time iOS credential setup usually requires interactive mode:

hstack stack eas happier ios --profile production --interactive

After 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-interactive

Common pitfalls

  • eas.json is not valid / "build.base" must be of type object:
    • your eas.json is using an old schema; update build.base to an object and use "extends": "base".
  • Credentials are not set up. Run this command again in interactive mode.:
    • run the same build with --interactive once to configure iOS signing.
  • Cloud build doesn’t see stack env vars:
    • set EAS environment variables (project settings) and make sure your profile has "environment": "production".

On this page