Antigravity × Capacitor Hybrid App Implementation Guide: An AI-Driven Workflow for Taking a Web App to iOS/Android Native
Practical implementation notes for porting an existing web app to iOS/Android native with Capacitor and Antigravity, grounded in lessons from years of running indie apps in production. Covers native API integration, AdMob tuning, and App Store / Google Play submission with measured numbers throughout.
Setup and context: What Years of Indie App Operations Taught Me About Capacitor's Sweet Spot
I've been shipping iOS/Android apps as an indie developer since 2013 — wallpaper apps, healing apps, manifestation apps, spanning a few genres. Throughout that run, "how do I extend my existing web app to mobile without burning months" has been a recurring headache. Rewriting in Flutter was expensive. React Native's native-bridge crashes ate into my weekends. Wiring up AdMob and Firebase Crashlytics SDKs by hand was a slog for a solo developer.
So when the combination of Antigravity (Google's AI-first IDE) and Capacitor showed up, I was skeptical. Could a WebView-based app really pass App Store review? Would Crashlytics actually work? Would AdMob mediation collapse? After porting an existing Next.js app to iOS/Android with Capacitor, the answer turned out to be much more practical than I expected.
A few real numbers worth sharing up front. For a meditation tool that took four months to build as a web app, the additional native-conversion effort came in at roughly 18 hours (of which about 5 hours were spent cleaning up Antigravity-generated code). It passed first-attempt review on both App Store and Google Play. The startup crash rate that landed in Crashlytics was 0.04%, and the only rejection reason was "Sign in with Apple return URL misconfiguration" — nothing intrinsic to the WebView itself.
The negative space is just as clear. Capacitor is the wrong tool for casual games that demand 60fps frame budgets, real-time video filters, or wallpaper apps that lean heavily on 3D rendering. For those, biting the bullet on SwiftUI / Jetpack Compose is faster end-to-end and gives a better result.
This guide starts by drawing those lines — where Capacitor wins, where it loses — and then walks through the implementation steps for taking a web app native with Antigravity as your pair programmer. The code in every chapter has been verified in the same production environment that powers my live apps (same AdMob, Crashlytics, and Firebase Analytics setup). It is written for:
Indie developers and startup engineers with a working web app, hunting for the minimum-cost path to mobile
Engineers turning React/Vue admin dashboards or internal tools into mobile apps for their team
Developers with no native iOS/Android background who want to ship through App Store / Google Play end-to-end
Antigravity users who want to expand their daily-driver IDE into mobile territory
Chapter 1: Capacitor Basics and Why It Pairs Well with Antigravity
What Is Capacitor?
Capacitor is an open-source cross-platform runtime developed by the Ionic team. It wraps web apps (HTML/CSS/JS) in a WebView and provides a bridge layer that allows JavaScript to call native features like the camera, GPS, and push notifications.
Designed as a successor to Cordova, it has the following characteristics:
Web standards compliant: API design that closely follows web standards, unlike Cordova's proprietary plugin format
Direct native project management: Manages iOS (Xcode project) and Android (Gradle project) files directly
Add-on compatible: Works with any web framework, including Next.js, Nuxt, and SvelteKit
Why Antigravity Pairs So Well with Capacitor
Antigravity's strength lies in generating accurate code by loading large volumes of API documentation and code examples as context. Capacitor's plugin list is well-documented officially, and Antigravity can reference those type definitions to generate precise code.
Specifically, you can expect assistance with:
Auto-generating boilerplate code for plugin calls
Writing accurate conditional code for both iOS and Android platforms
Implementing permission request timing and UI handling following best practices
Generating native plugin implementations in Swift/Kotlin from scratch (when custom plugins are needed)
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Where Capacitor wins and where it loses, validated against real production indie apps
First, ask Antigravity to verify your environment:
Antigravity prompt:
"Check what tools I need to start a Capacitor project on my machine
and install anything that's missing.
Target: Node.js, CocoaPods, Android Studio, and Xcode CLI tools."
Required tools:
Node.js 20.x or higher
Xcode 16 or higher (for iOS builds, macOS only)
Android Studio Meerkat or higher
CocoaPods (iOS dependency management)
# Example setup commands Antigravity will auto-runnode -v # Confirm v20 or higherpod --version # Confirm 1.15 or higherxcodebuild -version # Confirm Xcode 16 or higher# If CocoaPods isn't installedsudo gem install cocoapods
Adding Capacitor to an Existing Web App
We'll use a Next.js app as our example:
# Run from the project rootnpm install @capacitor/core @capacitor/clinpx cap init# Inputs during initialization# App Name: MyApp# App ID: com.yourcompany.myapp (reverse domain format)# Web Dir: out (Next.js static output directory)
Ask Antigravity to generate the capacitor.config.ts configuration:
// capacitor.config.ts// Antigravity auto-adds WebViewSettings and other best-practice configsimport type { CapacitorConfig } from '@capacitor/cli';const config: CapacitorConfig = { appId: 'com.yourcompany.myapp', appName: 'MyApp', webDir: 'out', server: { // Use local server during development (remove in production) // url: 'http://192.168.1.x:3000', // cleartext: true, }, plugins: { SplashScreen: { launchShowDuration: 2000, launchAutoHide: true, backgroundColor: '#ffffff', androidSplashResourceName: 'splash', showSpinner: false, }, PushNotifications: { presentationOptions: ['badge', 'sound', 'alert'], }, },};export default config;
Adding iOS and Android Platforms
# Add iOS/Android platformsnpm install @capacitor/ios @capacitor/androidnpx cap add iosnpx cap add android# Build the project and sync to nativenpm run build # Next.js static exportnpx cap sync # Update native projects to latest state
Antigravity prompt when you hit an error:
"I ran capacitor sync and got the following error: [paste error]
What's causing this and how do I fix it?
Please also implement a prevention measure so it doesn't happen again."
Chapter 3: Implementing Major Native APIs
Camera Plugin Implementation
The camera is one of the most commonly used native features. When asking Antigravity to implement it, include permission requests, error handling, and preview display in your prompt.
For iOS, you need to add permission descriptions to Info.plist. Ask Antigravity to handle this:
<!-- Add to ios/App/App/Info.plist --><!-- Antigravity can auto-detect this requirement and suggest adding it --><key>NSCameraUsageDescription</key><string>Used for taking profile photos and uploading product images</string><key>NSPhotoLibraryUsageDescription</key><string>Used for selecting images from your gallery</string><key>NSPhotoLibraryAddUsageDescription</key><string>Used for saving captured photos to your camera roll</string>
For Android, add permissions to AndroidManifest.xml:
Push notifications significantly impact app retention rates. Use Antigravity to generate an implementation that works with both Firebase Cloud Messaging (FCM) and Apple Push Notification service (APNs).
npm install @capacitor/push-notificationsnpx cap sync
// src/services/pushNotifications.tsimport { PushNotifications, PushNotificationSchema, ActionPerformed } from '@capacitor/push-notifications';import { Capacitor } from '@capacitor/core';export class PushNotificationService { private static instance: PushNotificationService; private token: string | null = null; private constructor() {} static getInstance(): PushNotificationService { if (!PushNotificationService.instance) { PushNotificationService.instance = new PushNotificationService(); } return PushNotificationService.instance; } async initialize(onTokenReceived: (token: string) => void): Promise<void> { // Only runs on native platforms (skip web environment) if (!Capacitor.isNativePlatform()) { console.log('Push notifications are only available on native platforms'); return; } // Register listeners for notification events PushNotifications.addListener('registration', (token) => { this.token = token.value; console.log('Push registration token: ', token.value); onTokenReceived(token.value); }); PushNotifications.addListener('registrationError', (error) => { console.error('Push registration error: ', error.error); }); // Handle foreground notifications PushNotifications.addListener('pushNotificationReceived', (notification: PushNotificationSchema) => { console.log('Push notification received: ', notification); this.handleForegroundNotification(notification); }); // Handle notification tap actions PushNotifications.addListener('pushNotificationActionPerformed', (action: ActionPerformed) => { console.log('Push action performed: ', action); this.handleNotificationAction(action); }); // Request permissions and register await this.requestPermissionAndRegister(); } private async requestPermissionAndRegister(): Promise<void> { let permStatus = await PushNotifications.checkPermissions(); if (permStatus.receive === 'prompt') { permStatus = await PushNotifications.requestPermissions(); } if (permStatus.receive !== 'granted') { console.warn('Notification permission denied'); return; } await PushNotifications.register(); } private handleForegroundNotification(notification: PushNotificationSchema): void { // Handle foreground notification (show custom UI, etc.) const event = new CustomEvent('push-notification-received', { detail: notification, }); window.dispatchEvent(event); } private handleNotificationAction(action: ActionPerformed): void { // Deep link handling from notification const data = action.notification.data; if (data?.route) { window.location.href = data.route; } } getToken(): string | null { return this.token; } async removeAllListeners(): Promise<void> { await PushNotifications.removeAllListeners(); }}
Biometric Authentication (Face ID / Fingerprint) Implementation
An essential feature for security-conscious apps:
npm install @aparajita/capacitor-biometric-authnpx cap sync
The debugging workflow combined with Antigravity is as follows:
iOS Debugging (using Safari):
Run the app in iOS Simulator
Safari → Develop → Simulator → Select the app's WebView
Use Web Inspector to check the console, network, and performance
Android Debugging (using Chrome):
Run the app in Android Emulator
Open chrome://inspect in Chrome
Click on the target device's WebView to begin debugging
Example Antigravity debug prompt:
"I used Chrome's inspect on the Android emulator and see the following
error in the console: [paste error log]
What's causing this error and what's the fix?"
Chapter 6: App Store / Google Play Release Preparation
Preparing for App Store Submission (iOS)
iOS releases require the following preparation. Ask Antigravity to verify Xcode settings and do a final Info.plist check.
# Create a release build with Gradlecd android./gradlew bundleRelease # AAB format (recommended)# Or./gradlew assembleRelease # APK format
Ask Antigravity to generate the signing configuration:
// Add to android/app/build.gradle// Antigravity generates a config following security best practicesandroid { signingConfigs { release { // Do not include the keystore in version control // Read from environment variables or local.properties storeFile file(System.getenv('KEYSTORE_PATH') ?: '../keystore/release.keystore') storePassword System.getenv('KEYSTORE_PASSWORD') ?: '' keyAlias System.getenv('KEY_ALIAS') ?: 'release' keyPassword System.getenv('KEY_PASSWORD') ?: '' } } buildTypes { release { signingConfig signingConfigs.release minifyEnabled true // Code obfuscation proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' shrinkResources true // Remove unused resources } }}
Pre-Submission Code Review with Antigravity
Before submitting, ask Antigravity to review your code against review criteria:
Antigravity prompt:
"Before I submit this Capacitor app for App Store review,
please conduct a code review from the following perspectives:
1. Alignment between privacy policy and actual permission requests
2. Locations that require App Tracking Transparency (ATT) implementation
3. APIs used in this app that must be declared in the iOS 17 App Privacy Manifest
4. Code with a high risk of crashes
5. Compliance with Apple's Human Interface Guidelines
Please classify findings by severity: High, Medium, Low."
Chapter 7: Automating Continuous Delivery
CI/CD Pipeline with GitHub Actions
Ask Antigravity to generate a complete GitHub Actions workflow:
What's Not in the Docs: Production Lessons from Running Capacitor at Scale
This section is the part the official Capacitor docs don't cover — pitfalls I actually hit while running my own apps in production, ordered by how often I see other developers run into them. Think of this as the "last 10%" you need to check after Antigravity generates the bulk of your code.
1. Killing the white-screen flash right after WebView launch
If you use @capacitor/splash-screen with its default auto-hide behavior, low-end Android devices (especially entry-level handsets with 2 GB RAM or less) hide the splash before the WebView is fully initialized, leaving a ~300–800 ms white flash. It feels surprisingly bad on a real device and is a common driver of "app is slow to start" reviews.
The fix is to switch to a "Web side hides the splash explicitly once it's fully painted" pattern. Set launchAutoHide: false in capacitor.config.ts and call SplashScreen.hide() from the root component's useEffect.
// capacitor.config.tsimport { CapacitorConfig } from '@capacitor/cli';const config: CapacitorConfig = { appId: 'com.yourcompany.myapp', appName: 'MyApp', webDir: 'out', plugins: { SplashScreen: { launchAutoHide: false, // don't auto-hide backgroundColor: '#0b1020', // match the web background exactly androidScaleType: 'CENTER_CROP', }, },};export default config;
// src/app/layout.tsx or _app.tsximport { Capacitor } from '@capacitor/core';import { SplashScreen } from '@capacitor/splash-screen';import { useEffect } from 'react';export default function RootLayout({ children }: { children: React.ReactNode }) { useEffect(() => { if (!Capacitor.isNativePlatform()) return; // Call after fonts, images, and initial API requests are settled const t = setTimeout(() => { SplashScreen.hide({ fadeOutDuration: 200 }); }, 50); return () => clearTimeout(t); }, []); return <>{children}</>;}
In my measurements, this change alone eliminated the white flash on the Galaxy A05 (the lowest-spec device I support) and dropped Firebase Performance Monitoring's app_start metric from ~1.8 s to ~0.9 s on average.
2. Making AdMob behave correctly inside Capacitor
Dropping @capacitor-community/admob in as-is can backfire on iOS when combined with Sign in with Apple or In-App Purchase. If you call AdMob.initialize() before the ATT (App Tracking Transparency) prompt, IDFA isn't available on iOS 14+, and eCPM drops by roughly 35–40%.
The correct initialization order is below. When you ask Antigravity for AdMob integration, you have to spell this out — otherwise the generated code tends to request ATT after initializing AdMob.
import { AdMob } from '@capacitor-community/admob';import { AppTrackingTransparency } from 'capacitor-plugin-app-tracking-transparency';export async function initAds() { // 1) Show ATT prompt first (only iOS 14.5+ shows the actual dialog) const att = await AppTrackingTransparency.requestPermission(); // 2) Personalized ads only if authorized; otherwise non-personalized const npa = att.status === 'authorized' ? '0' : '1'; // 3) Initialize AdMob *after* the ATT result is known await AdMob.initialize({ requestTrackingAuthorization: false, // already handled above testingDevices: [], initializeForTesting: false, }); // 4) Pass npa with each ad request await AdMob.bannerAd.show({ adId: 'ca-app-pub-XXXXXXXXXXXXXXXX/YYYYYYYYYY', adSize: 'ADAPTIVE_BANNER', position: 'BOTTOM_CENTER', margin: 0, npa, } as any);}
Real-world result: switching one of my wallpaper apps to this initialization order recovered iOS eCPM from $1.42 to $2.11 (+48%).
3. Minimal bridge to send WebView JavaScript errors to Crashlytics
In a Capacitor app, Crashlytics only sees native crashes by default — JavaScript errors inside the WebView are silently dropped. Leave this alone and you'll keep getting "Crashlytics is clean but users say the app crashed" reports.
The fix is a tiny bridge: catch window.onerror and unhandledrejection on the web side and forward them to @capacitor-community/firebase-crashlytics's recordException. Antigravity will spit out a version in 30 seconds, but make sure the shape is something like this:
In production, this took my "invisible JavaScript error" volume from ~18/week to 0, and the "screen freezes on this view" reviews disappeared within two weeks.
4. Defending against WKWebView Cookie and localStorage loss on iOS
iOS WKWebView can clear Cookies and localStorage without warning under memory pressure. This is the typical root cause of "I randomly get logged out every few days."
The defense is two-fold:
Store auth tokens in @capacitor/preferences and rehydrate them into localStorage on app launch
If you depend on session Cookies, keep limitsNavigationsToAppBoundDomains set to false and enable persistence on WKHTTPCookieStore
import { Preferences } from '@capacitor/preferences';const KEY = 'auth.idToken';export async function persistToken(token: string) { await Preferences.set({ key: KEY, value: token }); localStorage.setItem(KEY, token); // keep parity within the same session}export async function restoreTokenOnBoot() { const { value } = await Preferences.get({ key: KEY }); if (value && !localStorage.getItem(KEY)) localStorage.setItem(KEY, value);}
After rolling this out in one of my manifestation apps, support tickets reading "I keep getting asked to log in again" dropped from about 24/month to 3/month.
5. Three pre-submission checks that consistently get apps rejected
Antigravity's generated code mostly runs as-is, but it doesn't know about store-review policy quirks. Based on actual rejections I've eaten, these are the three checks you have to do before pushing the binary:
Sign in with Apple return URL: If you set up Universal Links via Capacitor but forget to host apple-app-site-association, you get rejected. Serve https://yourdomain.com/.well-known/apple-app-site-association with Content-Type: application/json.
Privacy manifest (iOS 17+): Declare the tracking-relevant APIs Capacitor uses (e.g., NSPrivacyAccessedAPICategoryUserDefaults) in PrivacyInfo.xcprivacy. Missing this used to be a warning; in 2024 it became a rejection reason.
Google Play data-deletion link: Users must be able to delete their account both inside and outside the app. Implement an account-deletion screen inside the Capacitor WebView and register its URL in Play Console's "Data safety" form.
Confirming these three before submission, in my experience, raises first-attempt approval rate from roughly 30% to 90%+.
6. Pre-release checklist (the one I actually use)
Finally, here's the checklist I run through before submitting to either store. This is the human-side pass that comes before you ask Antigravity to "audit this repo for Capacitor release readiness."
[ ] appId in capacitor.config.ts matches the Bundle ID on App Store Connect / Play Console exactly
[ ] webDir output contains index.html and next.config.ts has output: 'export' set
[ ] iOS: every plugin's usage-description string (e.g., NSCameraUsageDescription) is filled into Info.plist
[ ] iOS: PrivacyInfo.xcprivacy is placed in ios/App/App/
[ ] Android: AndroidManifest.xml requests only the minimum permissions beyond INTERNET
[ ] AdMob: test ad unit IDs (ca-app-pub-3940256099942544/...) are not left in the production build
[ ] Crashlytics: dSYM upload to Firebase is wired into the build phase
[ ] Splash Screen: launchAutoHide: false is in place so hiding is controlled by the web side
[ ] Live Reload: server.url is not set in the production build (it's a security risk if it leaks)
[ ] App Store: Sign in with Apple flow has been tested on a real device (some bugs only show up off-simulator)
This list is the residue of submitting 50+ apps through review — it's everything I keep tripping on, distilled. Use Antigravity to do the heavy lifting, but run human eyes over this list at the end. It saves you the 1–2 weeks you lose on a rejection.
Summary
By combining Capacitor with Antigravity, even developers with only web development experience can now release high-quality iOS and Android native apps.
Here's a recap of what this guide covered:
Capacitor's core concepts and how to integrate it with Next.js
Implementing native APIs — camera, push notifications, and biometric auth
Data persistence with Preferences and SQLite
WebView performance optimization and debugging techniques
Build configurations for App Store and Google Play submission
Full CI/CD automation with GitHub Actions
With Antigravity's assistance, code generation, error resolution, and best practice implementation are dramatically more efficient at each step. Even complex native configuration can be largely automated with natural language instructions like "please implement [feature]."
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.