Vibe code to app stores

Android & iOS on macOS

18 min read

A step-by-step guide for shipping a vibe-coded web app to the Google Play Store and/or the Apple App Store from a Mac — covering Xcode and Android Studio setup, Capacitor for both platforms, signing, and the full submission process for each store.

If you're using Lunadeck, the build and signing steps are handled for you automatically. This guide is for building and publishing locally, or for understanding what Lunadeck does under the hood.

What you'll need before starting:

  • A Mac running macOS Sequoia 15.6 or later (required for Xcode 26)
  • At least 30 GB of free disk space (Xcode ~12 GB, Android Studio ~10 GB, plus SDKs and simulators)
  • At least 8 GB of RAM (16 GB recommended)
  • A credit/debit card ($25 one-time for Google Play; $99/year for Apple Developer Program)
  • Your exported web app project (ZIP from Bolt.new, Lovable, etc.)
  • About 2–4 hours for initial setup, then 1–4 weeks for publishing

Before you get excited about the App Store: Apple's Guideline 4.2 (Minimum Functionality) explicitly states that apps must "include features, content, and UI that elevate it beyond a repackaged website." A plain Capacitor WebView wrapping your web app will almost certainly be rejected. You need at least some native integration (push notifications, biometric login, native navigation) to pass review. The Android side is far more forgiving. See Part 10 for details.


Part 1: Install the Required Tools

You need: Xcode (iOS builds), Android Studio (Android builds), Node.js, Java (JDK 17), and Capacitor (installed in Part 3). Homebrew handles most of this.

1.1 Install Homebrew

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

After installation, follow the printed instructions to add Homebrew to your PATH. On Apple Silicon:

echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

Verify:

brew --version
brew install curl wget unzip git

1.2 Install Xcode (iOS builds only)

Capacitor 8 requires Xcode 26.0 or newer.

Step 1: Install from the Mac App Store — search for "Xcode" and install it (~12 GB download). Or download from https://developer.apple.com/xcode/.

Step 2: Install command line tools:

xcode-select --install
sudo xcodebuild -license accept

Verify:

xcode-select -p
# Should output: /Applications/Xcode.app/Contents/Developer

Step 3: Open Xcode once and let it install additional components (simulators, etc.) before continuing.

1.3 Install Node.js (v22+)

Option A: Using nvm (recommended)

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.zshrc
nvm install 22
nvm use 22
node --version   # Should show v22.x.x

Option B: Using Homebrew

brew install node@22
node --version

1.4 Install Java (JDK 17) — Android only

brew install openjdk@17
 
# Symlink so macOS can find it
sudo ln -sfn $(brew --prefix openjdk@17)/libexec/openjdk.jdk \
  /Library/Java/JavaVirtualMachines/openjdk-17.jdk
 
java -version

1.5 Install Android Studio — Android only

Step 1: Download the macOS .dmg from https://developer.android.com/studio, drag it to Applications, and launch it. On first launch, run through the setup wizard (choose Standard, let it download the SDK and emulator).

Step 2: Install SDK components — go to Tools > SDK Manager:

  • SDK Platforms: check Android 14 (API 34) or newer
  • SDK Tools: check Android SDK Build-Tools, Android SDK Command-line Tools, Android Emulator, Android SDK Platform-Tools

Step 3: Set environment variables — add to your ~/.zshrc:

export ANDROID_HOME="$HOME/Library/Android/sdk"
export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
export PATH="$PATH:$ANDROID_HOME/platform-tools"
export PATH="$PATH:$ANDROID_HOME/tools"
export PATH="$PATH:$ANDROID_HOME/tools/bin"
export PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin"

Then reload and verify:

source ~/.zshrc
adb --version

Apple Silicon note: The Android emulator runs natively on ARM. When creating virtual devices, pick ARM-based system images (not x86).


Part 2: Export and Prepare Your Web App

2.1 Export from your vibe coding tool

In Bolt.new: click the project title > Export > Download. Other tools have similar export options. Unzip:

mkdir ~/my-app
cd ~/my-app
unzip ~/Downloads/your-project.zip -d .

2.2 Identify your build output directory

Capacitor needs to know where your built app ends up:

FrameworkBuild commandOutput folder
Vite (React/Vue/Svelte)npm run builddist
Create React Appnpm run buildbuild
Angularng builddist/your-app
Next.js (static export)next buildout

2.3 Make sure your app builds

cd ~/my-app
npm install
npm run build

Common issues with vibe-coded exports:

  • Missing .env variables — create a .env file with the needed API keys
  • Dependency conflicts — delete node_modules and package-lock.json, then re-run npm install
  • TypeScript errors — fix the specific file and line the error points to

Note: If your app has a backend (API routes, database), Capacitor wraps the frontend only. Your backend still needs to be hosted separately (Supabase, Railway, etc.) and your app must point to that hosted URL.


Part 3: Add Capacitor to Your Project

3.1 Install Capacitor

cd ~/my-app
npm install @capacitor/core @capacitor/cli

3.2 Initialize Capacitor

npx cap init

You'll be prompted for:

  • App name — the name users see under the icon
  • App Package ID — a unique reverse-domain identifier like com.yourname.myapp. Permanent — cannot be changed after publishing.
  • Web asset directory — the build output folder from Part 2 (usually dist or build)

Verify the generated capacitor.config.ts:

import type { CapacitorConfig } from '@capacitor/cli';
 
const config: CapacitorConfig = {
  appId: 'com.yourname.myapp',
  appName: 'My App',
  webDir: 'dist'   // Must match your actual build output folder
};
 
export default config;

Critical: webDir must point to the folder containing your built index.html. A wrong value produces a blank screen.

3.3 Add platforms

# Android
npm install @capacitor/android
npm run build
npx cap add android
 
# iOS
npm install @capacitor/ios
npx cap add ios

This creates android/ and ios/ folders — complete native projects.

Capacitor 8 iOS note: Capacitor 8 defaults to Swift Package Manager (SPM) for iOS, replacing CocoaPods (the default in Capacitor 7 and earlier). SPM is the recommended path going forward. If you need CocoaPods for a plugin that doesn't support SPM yet, use npx cap add ios --packagemanager CocoaPods.

3.4 Sync web code to both native projects

Run this every time you change your web code:

npm run build
npx cap sync

Part 4: Test Your App

4.1 Test on Android

npx cap open android

The first open downloads Gradle dependencies — takes several minutes. Do not upgrade Gradle if prompted.

Emulator: In Android Studio > Device Manager, create a virtual device (Pixel 7, API 34+, pick the ARM image). Click Run (▶).

Physical device: Enable Developer Options (Settings > About Phone > tap Build Number 7 times), enable USB Debugging, connect via USB, accept the prompt.

4.2 Test on iOS

npx cap open ios

Simulator: In Xcode, select a simulator (e.g., "iPhone 16") from the device dropdown and press Cmd + R.

Physical device: Connect your iPhone. In Xcode, go to the "App" target > Signing & Capabilities > select your Apple team. A free Apple ID works for testing (provisioning profiles expire every 7 days with a free account; the paid Developer Program removes this limit and is required for App Store distribution).

4.3 Common issues

Blank white screenwebDir is wrong or you forgot npm run build before npx cap sync.

App crashes immediately — check Logcat in Android Studio (View > Tool Windows > Logcat) or the Xcode console (View > Debug Area > Activate Console).

API calls failing — on the Android emulator, localhost refers to the emulator itself; use 10.0.2.2 to reach your Mac. On the iOS Simulator, localhost works normally since it shares your Mac's network.

iOS signing errors — make sure a signing team is selected in Xcode > Signing & Capabilities.


Part 5: Prepare for Release

5.1 App icon and splash screen

npm install @capacitor/assets --save-dev

Create an assets/ folder at the project root and add:

  • icon-only.png — at least 1024×1024 px, square, no transparency
  • icon-foreground.png — adaptive icon foreground (1024×1024 px)
  • icon-background.png — adaptive icon background (1024×1024 px)
  • splash.png — at least 2732×2732 px
  • splash-dark.png — dark mode splash (same size)

Generate all variants for both platforms:

npx capacitor-assets generate

iOS icon note: Apple requires your App Store icon to have no transparency and no rounded corners — they apply the rounding automatically.

5.2 Update version numbers

Android — in android/app/build.gradle:

versionCode 1       // Integer, must increase with every Play Store upload
versionName "1.0"   // Human-readable string shown to users

iOS — in Xcode (App target > General tab):

  • Version: user-visible version number (e.g., 1.0.0) — equivalent of versionName
  • Build: incrementing number per upload (e.g., 1) — equivalent of versionCode

Or edit ios/App/App/Info.plist directly:

<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>

5.3 App permissions

Android — in android/app/src/main/AndroidManifest.xml, inside <manifest>:

<!-- Internet access (included by default in Capacitor, but verify) -->
<uses-permission android:name="android.permission.INTERNET" />
 
<!-- Add only what your app actually uses: -->
<!-- <uses-permission android:name="android.permission.CAMERA" /> -->
<!-- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> -->
<!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> -->

iOS — in ios/App/App/Info.plist, add usage description strings for each permission. Apple will reject your app if these are missing or vague:

<!-- Add only for permissions your app actually uses: -->
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to take photos for your profile.</string>
 
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses your location to show nearby results.</string>
 
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone for voice messages.</string>
 
<key>NSPhotoLibraryUsageDescription</key>
<string>This app accesses your photo library to let you choose images.</string>

Part 6: Sign and Build the Android Release Bundle

6.1 Generate a signing key

keytool -genkey -v -keystore ~/my-app-release.keystore \
  -alias my-app-key \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000

Back up this keystore file and both passwords. Losing it means you can never update your app on Google Play. Store a copy in a password manager and on external storage.

6.2 Configure Gradle signing

Create android/keystore.properties (do NOT commit this to git — add it to .gitignore):

storeFile=/Users/YOUR_USERNAME/my-app-release.keystore
storePassword=your_keystore_password
keyAlias=my-app-key
keyPassword=your_key_password

Edit android/app/build.gradle — add above the android { block:

def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

Inside the android { block:

android {
    // ... existing config ...
 
    signingConfigs {
        release {
            storeFile file(keystoreProperties['storeFile'])
            storePassword keystoreProperties['storePassword']
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
        }
    }
 
    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

6.3 Build the release bundle

cd ~/my-app
npm run build
npx cap sync
 
cd android
./gradlew bundleRelease

Output: android/app/build/outputs/bundle/release/app-release.aab


Part 7: Sign and Build the iOS Release Archive

iOS code signing requires an Apple Developer Program membership ($99/year).

7.1 Enroll in the Apple Developer Program

  1. Go to https://developer.apple.com/programs/
  2. Click "Enroll" and sign in with your Apple ID
  3. Choose Individual or Organization (Individual shows your legal name as seller; Organization requires a D-U-N-S number)
  4. Pay the $99/year fee
  5. Wait for identity verification (a few hours to a few days)

7.2 Configure signing in Xcode

  1. npx cap open ios
  2. Select the "App" target in the project navigator
  3. Go to Signing & Capabilities
  4. Check "Automatically manage signing" and select your Apple Developer team

Xcode will create a provisioning profile and signing certificate automatically.

7.3 Build the archive

Option A: Xcode GUI (easier)

  1. Set the device target to "Any iOS Device (arm64)" in the dropdown — not a simulator
  2. Go to Product > Archive
  3. When the Organizer window opens, select your archive and click Distribute App
  4. Choose App Store Connect > Upload and follow the prompts

Option B: Command line

cd ~/my-app
npm run build
npx cap sync
 
cd ios/App
 
xcodebuild clean archive \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -archivePath ~/my-app-archive.xcarchive \
  -destination 'generic/platform=iOS'

Create ExportOptions.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>app-store</string>
    <key>uploadSymbols</key>
    <true/>
</dict>
</plist>

Export the IPA:

xcodebuild -exportArchive \
  -archivePath ~/my-app-archive.xcarchive \
  -exportPath ~/my-app-ipa/ \
  -exportOptionsPlist ExportOptions.plist

7.4 Upload to App Store Connect

  • From Xcode Organizer: available after using Product > Archive
  • Transporter app: free on the Mac App Store — drag and drop your IPA
  • Command line: xcrun altool --upload-app --type ios --file ~/my-app-ipa/App.ipa --apiKey YOUR_KEY_ID --apiIssuer YOUR_ISSUER_ID (API key created in App Store Connect under Users and Access > Integrations)

Part 8: Developer Accounts and Store Listings

8.1 Google Play Developer Account

  1. Go to https://play.google.com/console and sign in
  2. Choose Personal or Organization (Personal is fine for indie developers)
  3. Pay the one-time $25 USD fee and complete identity verification
  4. Wait 24–48 hours for activation

Personal accounts created after November 2023 require device verification. Install the Google Play Console mobile app on an Android device, sign in, and follow the steps.

8.2 App Store Connect

  1. Go to https://appstoreconnect.apple.com and sign in
  2. Click + > New App
  3. Fill in: Platform (iOS), Name, Primary language, Bundle ID (should match capacitor.config.ts appId), and an internal SKU
  4. Click Create

Required fields for your app page:

  • Description — short and full
  • Screenshots — at minimum for 6.7" (iPhone 15 Pro Max) and 6.1" (iPhone 15 Pro); capture from the Simulator
  • App icon — 1024×1024 px, no transparency, no rounded corners
  • Category
  • Privacy policy URL — mandatory; a simple Notion or GitHub Pages page works
  • Age rating — fill out the content questionnaire

Part 9: Google Play — The Closed Testing Requirement

New personal developer accounts cannot publish directly to production. Google requires:

  1. At least 12 testers opted in to a closed test track
  2. Those testers remain opted in for 14 consecutive days
  3. Then you can apply for production access

Set up closed testing

  1. In Play Console, click Create app and work through all required sections (store listing, content rating, target audience, privacy policy)
  2. Go to Testing > Closed testing, create a tester email list with 12+ Gmail addresses
  3. Click Create new release, accept Play App Signing, upload your .aab, add release notes, click Start rollout
  4. Share the generated opt-in link with your testers — they must click it on an Android device, accept the invitation, and install the app

After 14 days with all testers opted in, go to the app Dashboard and click Apply for production access.

Finding testers: Ask friends, family, colleagues. Developer communities on Discord and Reddit (r/androiddev, r/betatesting) sometimes organize tester exchanges. Emulators typically don't count — you need real devices.


Part 10: App Store — Review Process and Guideline 4.2

Apple's review is faster than Google's testing requirement (most reviews complete within 24 hours), but the bar for WebView-based apps is significantly higher.

The Guideline 4.2 problem

Apple Guideline 4.2 (Minimum Functionality) requires apps to "include features, content, and UI that elevate it beyond a repackaged website." A plain Capacitor WebView with no native integration will be rejected. This is not a theoretical risk — it is the most common rejection reason for apps like yours.

What to add to pass review — at minimum, consider:

  • Push notifications via @capacitor/push-notifications — the strongest differentiator, since Safari doesn't support web push in the same way
  • Biometric authentication (Face ID / Touch ID) via @capacitor/biometrics or similar
  • Status bar customization via @capacitor/status-bar
  • Haptic feedback via @capacitor/haptics

The more native integration you add, the better your chances — and the better the app actually feels.

TestFlight (beta testing)

Before submitting to production, use TestFlight:

  1. Upload a build to App Store Connect
  2. Go to the TestFlight tab
  3. Internal testers (up to 100 App Store Connect team members): available almost immediately, no review needed
  4. External testers (up to 10,000 via email or public link): the first build of each version requires a brief beta review (~24 hours)

TestFlight builds expire after 90 days.

Submitting for production review

  1. In App Store Connect, select your build
  2. Fill out App Review Information — if your app has a login, provide demo credentials
  3. In the notes for reviewers, explicitly list your native integrations and what the app does — this is your chance to proactively address Guideline 4.2
  4. Click Submit for Review

Apple reviews ~90% of submissions within 24 hours. Complex apps or those triggering certain guidelines can take 2–5 days.

If your app gets rejected

Read the rejection reason carefully. Common fixes:

  • Guideline 4.2: Add native features (push notifications, biometric auth, native navigation). Resubmit.
  • Missing privacy policy: Add a URL to a hosted privacy policy.
  • Missing usage descriptions: Add the NS...UsageDescription keys to Info.plist.
  • Crashes during review: Fix the crash — reviewer crash logs appear in the Resolution Center in App Store Connect.

Part 11: Publish to Production

Google Play

Once you have production access:

  1. Go to Production > Create new release
  2. Upload your .aab, add release notes, select distribution countries
  3. Click Start rollout to production

App Store

Once Apple approves your submission, choose:

  • Immediate release — goes live as soon as approved (default)
  • Manual release — you choose when to publish
  • Scheduled release — set a specific date

Part 12: Updating Your App

# 1. Make your code changes
# 2. Rebuild and sync
npm run build
npx cap sync
 
# 3. Bump version numbers:
#    Android: versionCode in android/app/build.gradle
#    iOS: Build number in Xcode or Info.plist
 
# Android
cd android
./gradlew bundleRelease
# Upload new .aab to Google Play Console
 
# iOS
cd ../ios/App
# Product > Archive > Distribute in Xcode, or:
xcodebuild archive -workspace App.xcworkspace -scheme App \
  -archivePath ~/my-app.xcarchive -destination 'generic/platform=iOS'
xcodebuild -exportArchive -archivePath ~/my-app.xcarchive \
  -exportPath ~/my-app-ipa/ -exportOptionsPlist ExportOptions.plist
# Upload via Transporter or xcrun altool

Quick Reference

# === ONE-TIME SETUP ===
npm install @capacitor/core @capacitor/cli
npx cap init
npm install @capacitor/android @capacitor/ios
npx cap add android
npx cap add ios
 
# === EVERY CODE CHANGE ===
npm run build
npx cap sync
 
# === TESTING ===
npx cap open android          # Open in Android Studio
npx cap open ios              # Open in Xcode
npx cap run android           # Run on Android device/emulator
npx cap run ios               # Run on iOS device/simulator
 
# === ANDROID RELEASE ===
cd android && ./gradlew bundleRelease
# Output: android/app/build/outputs/bundle/release/app-release.aab
 
# === iOS RELEASE (Xcode GUI) ===
# Product > Archive > Distribute App > App Store Connect > Upload
 
# === iOS RELEASE (command line) ===
cd ios/App
xcodebuild archive -workspace App.xcworkspace -scheme App \
  -archivePath ~/my-app.xcarchive -destination 'generic/platform=iOS'
xcodebuild -exportArchive -archivePath ~/my-app.xcarchive \
  -exportPath ~/my-app-ipa/ -exportOptionsPlist ExportOptions.plist

Troubleshooting

"SDK location not found" (Android)ANDROID_HOME is not set. Follow Part 1.5 Step 3 and run source ~/.zshrc.

Xcode build fails with "No signing certificate" — select your Apple Developer team in Xcode > Signing & Capabilities. Apple Developer Program membership ($99/year) is required for distribution builds.

"JAVA_HOME is not set" (Android) — add to ~/.zshrc:

export JAVA_HOME=$(/usr/libexec/java_home -v 17)

Then run source ~/.zshrc.

Gradle fails with "Could not determine java version" — multiple Java versions installed. Use /usr/libexec/java_home -V to list them and confirm JAVA_HOME points to JDK 17.

"capacitor.config.ts: webDir does not exist" — run npm run build first.

App shows blank screen — verify webDir matches the actual build output and that index.html is inside it.

"INSTALL_FAILED_UPDATE_INCOMPATIBLE" (Android) — uninstall the old version from your device.

iOS Simulator is slow — on Apple Silicon it should be fast. Try Device > Erase All Content and Settings in the Simulator menu. Close other heavy apps — Xcode + Android Studio + Chrome eat RAM quickly.

SPM plugin compatibility warning — some third-party Capacitor plugins haven't migrated to Swift Package Manager. Use npx cap add ios --packagemanager CocoaPods as a fallback, or find an alternative plugin that supports SPM.

"The request was denied by service delegate" when archiving — provisioning profile or certificate is invalid. Go to Xcode > Settings > Accounts > select your team > Download Manual Profiles. Or toggle "Automatically manage signing" off and back on.


Realistic Expectations

The iOS bar is higher than Android's. Android accepts almost any working AAB. Apple actively rejects WebView wrappers that don't add native value. If you're publishing to one platform first, Android is dramatically easier. Plan extra time for native iOS integration to pass Guideline 4.2.

The costs are different. Google Play: $25 one-time. Apple: $99 per year, every year. If your Apple Developer membership lapses, your apps are removed from the App Store within 24 hours.

The Google Play timeline is long. The 14-day testing requirement plus review means 3–4 weeks from start to live. Apple's timeline is shorter (a few days if you pass first try) but rejections add cycles.

Capacitor wraps, it doesn't transform. Your app behaves exactly like it does in a mobile browser. Test in Chrome's mobile emulation mode and fix layout issues before wrapping.

Budget for at least one rejection cycle on iOS. Don't treat the App Store the same as the Play Store. A rejection isn't the end — read it carefully, add native features, and resubmit.