Antigravity × Capacitor Hybrid App Implementation Guide: An AI-Driven Workflow for Taking a Web App to iOS/Android Native
Implementation notes for taking an existing web app to iOS/Android with Capacitor and Antigravity, drawn from running indie apps. Covers native API integration, AdMob initialization order, launch-time white flash, and pre-submission checks, separating what is verified from what is not.
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 for a long time
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? Wrapping an existing Next.js app with Capacitor turned out to be more practical than I expected — provided the app is the kind that suits it.
First, a word about the figures you will find below. An earlier version listed specific figures — effort hours, crash rate, eCPM gains — and on a second look some of them came without measurement conditions I could reproduce. In this revision I have removed every figure I cannot back up. What remains is the reasoning behind each order and setting, and facts you can check in code.
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, I believe SwiftUI / Jetpack Compose is faster end-to-end and gives a better result.
This guide starts by drawing those lines, then walks through the implementation steps for taking a web app native with Antigravity as your pair programmer. The code follows the official docs and each plugin's README, but please verify it against your own Capacitor and plugin versions. 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 fits and where it does not, plus the pitfalls indie developers tend to hit
✦Implementation examples: ATT-before-AdMob ordering, manual Splash Screen control, and bridging web errors to Crashlytics
✦A checklist of items to verify by hand before App Store / Google Play submission
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
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}</>;}
The effect depends on the device and on what your web side initializes at launch. Compare app_start in Firebase Performance Monitoring before and after, on the same device.
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'; // true = non-personalized ads // 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);}
Initializing before the ATT result arrives can mean no IDFA and weaker personalized-ad delivery, which may lower revenue. The size of the drop depends on your setup, so compare AdMob reports before and after.
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:
With this in place, JavaScript exceptions become visible in Crashlytics too. The aim is to close the gap where Crashlytics shows nothing but users say the app froze.
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, verify Cookie persistence on the WKWebView side (limitsNavigationsToAppBoundDomains is for App-Bound Domains and restricts navigation when true)
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 can cut down the rejection round trips.
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 keeps only the items I wished I had checked first after a rejection. Use Antigravity to do the heavy lifting, but run human eyes over it at the end — it helps avoid the week or two lost to a rejection.
Summary
Combining Capacitor with Antigravity makes it easier for a developer with a web background to step into native iOS/Android apps.
As a next step, try just one of the "last 10%" items on an existing app — manual Splash Screen control, say — and time the launch before and after on the same device.
Antigravity can draft even the native configuration that looks intimidating when you ask it to "please implement [feature]". Permission strings, store policies, and signing are still worth checking one by one with your own eyes.
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.