Capacitor v7 iOS/Android native app development. Use when working on native mobile features, Apple/Google Sign-In, push notifications (FCM), haptics, deep linking, safe area handling, Xcode Cloud builds, or platform-conditional code. Covers Capacitor plugin patterns, native bridge communication, and App Store submission. Do NOT use for web-only features or server-side changes.
Capacitor v7 bridge wrapping the Nuxt 3 web app as a native iOS/Android app. Bundle ID: com.getminds.app. Production API: https://getminds.ai.
capacitor.config.ts defines the bridge setup:
{ appId: 'com.getminds.app', appName: 'Minds AI', webDir: '.output/public', bundledWebRuntime: false,
ios: {
scheme: 'App',
webViewConfiguration: {
allowsInlineMediaPlayback: true, // Enables video playback inside WebView (required for avatar)
mediaTypesRequiringUserActionForPlayback: 'none' // Allows autoplay for avatar video tracks
}
},
plugins: {
SplashScreen: { launchShowDuration: 2000, backgroundColor: '#000000', showSpinner: false },
PushNotifications: { presentationOptions: ['badge', 'sound', 'alert'] }
}
}
webDir: '.output/public' -- output of nuxt generate (static SSG build)App means the WebView loads from capacitor://localhosthttp://localhost:3000usePlatform() (preferred for components)Singleton-cached computed refs. Use in components and templates.
const { isNative, isIOS, isAndroid, isWeb, platform } = usePlatform()
// All are computed refs: isNative.value, isIOS.value, etc.
useNative() (for native API access)Returns plain booleans (not refs) plus haptics, status bar, and safe area APIs.
const { isNative, isIOS, isAndroid, vibrate, vibrateSuccess, setDarkStatusBar, safeAreaInsets, initNative } = useNative()
// isNative is a plain boolean (Capacitor.isNativePlatform())
useWebViewDetection() (for OAuth warnings)Detects problematic in-app browsers (Instagram, Facebook, LinkedIn, etc.) that break OAuth. Capacitor native apps return false since they use the system browser for OAuth.
const { isInWebView, getBrowserName, getWebViewWarningMessage } = useWebViewDetection()
useMobile() (responsive breakpoint only)Pure CSS breakpoint detection (window.innerWidth < 768). Not related to native -- works on web too.
const { isMobile, MOBILE_BREAKPOINT } = useMobile() // MOBILE_BREAKPOINT = 768
| Plugin | Version | Purpose | Used In |
|---|---|---|---|
@capacitor/core | ^7.4.4 | Core bridge, Capacitor.isNativePlatform() | Everywhere |
@capacitor/ios | ^7.4.4 | iOS runtime | Native build |
@capacitor/android | ^7.4.4 | Android runtime | Native build |
@capacitor/cli | ^7.4.4 | CLI tooling (cap sync, cap open) | Build scripts |
@capacitor/app | ^7.1.0 | App lifecycle, deep links, back button | plugins/capacitor-app.client.ts |
@capacitor/push-notifications | ^7.0.0 | Push registration and handling | plugins/push-notifications.client.ts, composables/core/usePushNotifications.ts |
@capacitor/haptics | ^7.0.2 | Haptic feedback (impact, notification, selection) | composables/core/useNative.ts |
@capacitor/status-bar | ^7.0.3 | Status bar style and color | composables/core/useNative.ts, plugins/capacitor-app.client.ts |
@capacitor/splash-screen | ^7.0.4 | Launch splash screen (2s, black) | capacitor.config.ts |
@capacitor/preferences | ^7.0.3 | Native key-value storage for auth sessions | utils/capacitor-storage.ts, plugins/supabase-storage.client.ts |
@capacitor/camera | ^7.0.3 | Photo capture/library access | components/common/ChatInput.vue |
@capacitor/browser | ^7.0.3 | System browser for OAuth flows | Referenced for OAuth |
@capacitor/keyboard | ^7.0.4 | Keyboard visibility and events | Native build |
@capacitor-community/apple-sign-in | ^7.1.0 | Native Apple Sign-In | composables/auth/useAuth.ts |
capacitor-native-google-one-tap-signin | ^7.0.3 | Native Google One Tap Sign-In | composables/auth/useAuth.ts |
@revenuecat/purchases-capacitor | ^11.3.2 | In-app purchases (iOS IAP, subscription management) | composables/iap/useRevenueCat.ts, plugins/capacitor-app.client.ts |
Additionally, iOS uses Firebase/Core and Firebase/Messaging CocoaPods for FCM token handling (see ios/App/Podfile).
Native apps cannot use cookie-based auth (WebView cookies do not sync with the server). Instead, all API requests use Bearer token auth.
useAuth().signInWithGoogle() detects isNative && platform === 'ios', calls signInWithGoogleNative() which uses capacitor-native-google-one-tap-signin. The ID token is exchanged with Supabase via signInWithIdToken(). Falls back to web OAuth if native fails.useAuth().signInWithApple() on iOS calls signInWithAppleNative() using @capacitor-community/apple-sign-in. ID token exchanged with Supabase.supabase.auth.signInWithOAuth(). The callback at /auth/confirm detects pkce_id query param (native OAuth indicator), exchanges code, then redirects back to the app via custom URL scheme com.getminds.app://auth/native-callback?access_token=...&refresh_token=..../pages/auth/native-callback.vue receives tokens from URL, calls supabase.auth.setSession() to establish the WebView session.plugins/supabase-storage.client.ts uses @capacitor/preferences to persist Supabase sessions across app restarts. On auth state changes, the session is written to native storage under key sb-session. On app launch, stored sessions are restored via supabase.auth.setSession().
plugins/auth-fetch.client.ts (runs with enforce: 'pre') intercepts all $fetch calls on native:
Authorization: Bearer <token> header to /api/* requests/api/* URLs to https://getminds.ai/api/* (since the app loads from capacitor://localhost)For streaming requests, use authStreamFetch() from utils/auth-fetch.ts which applies the same token injection and URL rewriting to native fetch() calls.
server/middleware/cors-native.global.ts allows capacitor:// and ionic:// origins on /api/ routes, including preflight OPTIONS handling.
plugins/push-notifications.client.ts runs on native app startup, sets up listenersregistration event, POSTs token to /api/notifications/register with platformDeviceToken Prisma model (device_tokens table): { id, userId, token (unique), platform ('ios'|'android'), createdAt, updatedAt }registrationError listener logs failuresios/App/App/AppDelegate.swift configures Firebase, sets Messaging.messaging().delegate, and bridges APNS tokens to FCM tokens. FCM token is posted to Capacitor via NotificationCenter using .capacitorDidRegisterForRemoteNotifications.
pushNotificationReceived listener logs; iOS shows banner/badge/sound via UNUserNotificationCenterDelegatepushNotificationActionPerformed reads notification.data and navigates: sparkId -> /?sparkId=X, flowId -> /flows/X, daily_digest -> /composables/core/usePushNotifications.ts provides checkPermission(), requestPermission(), unregister(), registerIfPermitted(). Used in components/settings/general.vue for the notification toggle.
iOS subscription management via RevenueCat (@revenuecat/purchases-capacitor).
plugins/capacitor-app.client.ts initializes RevenueCat on iOS when a user is authenticateduseRevenueCat().initialize(userId) to configure RevenueCat with the user's IDappStateChange -> isActive), refreshes subscription state via useSubscription().loadSubscription(true) to pick up purchases made in the App Store purchase sheetcomposables/iap/useRevenueCat.ts)initialize(userId) -- Configures RevenueCat SDK with user ID via async loadPurchasesModule() (lazy Capacitor plugin loading)isPurchasing ref prevents double-purchase calls with logginguseSubscription.subscribe() throws error on iOS for lite/premium plans (only team plans allowed through Stripe on iOS)https://apps.apple.com/account/subscriptions directly (never Stripe portal)RevenuecatPurchasesCapacitor pod in ios/App/PodfileAll handled in plugins/capacitor-app.client.ts:
native-app and platform-{ios|android} to <html> for CSS targetingmaximum-scale=1, user-scalable=no, viewport-fit=cover to prevent zoom on input focusappUrlOpen listener parses custom scheme URLs (com.getminds.app://path) and navigates via Nuxt routeruseColorMode() -- dark mode gets Style.Light (white icons), light mode gets Style.Dark (black icons). Android also sets background color.appStateChange listener fires on foreground/background transitions. On iOS resume, refreshes subscription state via loadSubscription(true).App.minimizeApp()com.getminds.app:// configured in Info.plist via CFBundleURLTypes (auto-patched by scripts/capacitor-post-sync.mjs)App.entitlements declares applinks:getminds.ai, applinks:www.getminds.ai, applinks:staging.getminds.aiwebcredentials:getminds.ai and webcredentials:staging.getminds.ai for password autofill association/auth/confirm redirects back via com.getminds.app://auth/native-callback?access_token=...&refresh_token=...composables/ui/useMobileSwipe.ts -- call once in the main layout. Attaches document-level touch listeners.
layoutStore.mobileDragProgress (0-1) for smooth animationedgeThreshold (default 30px), swipeThreshold (default 80px)Provided by useNative() from composables/core/useNative.ts. All methods are no-ops on web.
| Method | Capacitor API | Use Case |
|---|---|---|
vibrate(style?) | Haptics.impact({ style }) | Default: ImpactStyle.Light |
vibrateSuccess() | Haptics.notification({ type: Success }) | Positive actions |
vibrateWarning() | Haptics.notification({ type: Warning }) | Caution states |
vibrateError() | Haptics.notification({ type: Error }) | Error states |
vibrateSelection() | Haptics.selectionStart/End | Selection changes |
assets/css/main.css):root {
--sat: env(safe-area-inset-top, 0px);
--sab: env(safe-area-inset-bottom, 0px);
--sal: env(safe-area-inset-left, 0px);
--sar: env(safe-area-inset-right, 0px);
}
.safe-area-top, .safe-area-bottom, .safe-area-left, .safe-area-right, .safe-area-all -- apply corresponding env(safe-area-inset-*) padding.keyboard-safe -- bottom padding for inputs near keyboard.native-viewport -- full viewport container (100dvh, 100% in native)html.native-app)overscroll-behavior: none)-webkit-text-size-adjust: 100%)backdrop-filter (causes WKWebView rendering issues).haptic-feedback:active adds subtle scale-down effect-webkit-tap-highlight-color: transparent)| Script | Action |
|---|---|
mobile:build | nuxt generate && npx cap sync && node scripts/capacitor-post-sync.mjs |
mobile:ios | Full build + open Xcode |
mobile:sync | Sync only (no web build) |
build:mobile | nuxt generate with Prisma generate (no engine) |
ios:build | build:mobile + CAPACITOR_BUILD=production cap sync ios |
ios:beta | Build + Fastlane beta (TestFlight, skip processing wait) |
ios:release | Build + Fastlane release (TestFlight + git version bump) |
scripts/capacitor-post-sync.mjs patches Info.plist and AndroidManifest.xml to add the com.getminds.app custom URL scheme for deep linking. Runs automatically after cap sync.
ios/App/fastlane/Fastfile defines two lanes:
app-store export, upload to TestFlightensure_git_status_clean, commit version bump, push to gitAPI key loaded from APP_STORE_KEY_ID, APP_STORE_ISSUER_ID, and either APP_STORE_KEY_CONTENT (CI) or AuthKey.p8 file (local).
ios/App/ci_scripts/ci_post_clone.sh handles CI builds: installs Node 22 via nvm, creates .env from Xcode Cloud env vars, runs npm ci, generates Prisma client (no engine), nuxt generate, cap sync ios, and pod install.
ios/App/)App/AppDelegate.swift -- Firebase init, push notification bridging (APNS -> FCM), URL handlingApp/Info.plist -- Bundle config, URL schemes, camera/photo permissions, orientationsApp/App.entitlements -- Push notifications (aps-environment), Sign in with Apple, associated domains (universal links)App/GoogleService-Info.plist -- Firebase config for FCMPodfile -- All Capacitor pods + Firebase/Core + Firebase/Messaging. Min iOS 14.0.fastlane/ -- Appfile (team ID 5Q7VWK48G5) + FastfileNo android/ directory currently exists in the repository. Android support is configured in package.json scripts but the native project has not been initialized.
// In components -- use usePlatform() (computed refs for reactivity)
const { isNative } = usePlatform()
// In templates: v-if="isNative"
// In plugins/utils -- use Capacitor directly (outside component context)
if (Capacitor.isNativePlatform()) { ... }
// CSS targeting
html.native-app .my-element { /* native-only styles */ }
html.platform-ios .my-element { /* iOS-only styles */ }
// Detecting native via DOM (for utilities outside Vue context)
document.documentElement.classList.contains('native-app')
// Native image URLs need token appended (useImageUrl composable)
const { normalizeImageUrl } = useImageUrl()
// Appends ?token=<access_token> and prepends API_BASE_URL in production native
auth-fetch.client.ts plugin handles this automatically for $fetch, but manual fetch() calls must use authStreamFetch().capacitor://localhost, so /api/* calls need rewriting to https://getminds.ai/api/*. This is automatic via the auth-fetch plugin, but any new fetch patterns must account for it.await import('@capacitor/push-notifications')) and guard with Capacitor.isNativePlatform() to prevent web crashes.safe-area-top/safe-area-bottom on fixed headers/footers in native. The viewport meta tag includes viewport-fit=cover.font-size: 16px !important on all inputs.backdrop-filter is disabled on native (html.native-app) due to WKWebView rendering bugs. Use solid background colors instead.@capacitor/preferences (native key-value store), not localStorage. The supabase-storage.client.ts plugin handles this.currentProtocol === 'capacitor:' or currentHost === 'localhost' to avoid noise from dev/native builds.pkce_id query parameter identifies native OAuth callbacks on /auth/confirm..then() interception -- Capacitor plugin proxies intercept .then(). Never return a Capacitor plugin object from an async function (causes "Purchases.then() is not implemented on ios"). Always extract the result before returning.capacitor.config.ts server URL -- The server.url field is a local-only dev setting (points to staging/prod for testing). Do not commit without asking -- it must be removed or reset for production builds.platform === 'ios'), not Android. Refreshes subscription state when app returns to foreground to catch App Store purchase sheet completions.GOOGLE_CLIENT_ID -- required for native Google One Tap sign-in (set as runtime config googleClientId)CAPACITOR_BUILD=production -- set during ios:build to signal production native buildSUPABASE_URL, SUPABASE_ANON_KEY, SITE_URL, plus optional analytics/payment keysGoogleService-Info.plist in iOS project for Firebase/FCM configurationcapacitor.config.ts -- Capacitor bridge configurationplugins/auth-fetch.client.ts -- Bearer token interceptor and URL rewriting for nativeplugins/supabase-storage.client.ts -- Session persistence via Capacitor Preferencesplugins/push-notifications.client.ts -- Push notification registration and handlingplugins/capacitor-app.client.ts -- App lifecycle, deep links, status bar, back buttonplugins/native-auth.client.ts -- Native auth plugin loaderutils/auth-fetch.ts -- authFetch() and authStreamFetch() for native API callsutils/capacitor-storage.ts -- Storage adapter (Preferences on native, localStorage on web)composables/core/useNative.ts -- Haptics, status bar, safe area, platform booleanscomposables/usePlatform.ts -- Singleton cached platform detection (computed refs)composables/core/usePushNotifications.ts -- Push permission/registration composablecomposables/ui/useWebViewDetection.ts -- In-app browser detection for OAuth warningscomposables/ui/useMobile.ts -- Responsive breakpoint detection (768px)composables/ui/useMobileSwipe.ts -- Swipe gesture handling for sidebarcomposables/ui/useImageUrl.ts -- Image URL normalization with native token injectioncomposables/useAuthFetch.ts -- Component-scoped auth fetch wrappercomposables/auth/useAuth.ts -- Auth methods including native Google/Apple sign-incomposables/iap/useRevenueCat.ts -- RevenueCat IAP initialization and purchase flowserver/middleware/cors-native.global.ts -- CORS headers for native originsmiddleware/auth.global.ts -- Auth middleware (includes /auth/native-callback as public route)pages/auth/confirm.vue -- OAuth callback with native app redirectpages/auth/native-callback.vue -- Receives tokens from Safari and establishes WebView sessionscripts/capacitor-post-sync.mjs -- Patches Info.plist/AndroidManifest for URL schemesassets/css/main.css -- Safe area utilities and html.native-app scoped stylesios/App/App/AppDelegate.swift -- Firebase, push notification bridging, URL handlingios/App/App/Info.plist -- Bundle config, permissions, URL schemesios/App/App/App.entitlements -- Push, Apple Sign-In, universal linksios/App/Podfile -- CocoaPods dependenciesios/App/fastlane/Fastfile -- TestFlight beta/release lanesios/App/ci_scripts/ci_post_clone.sh -- Xcode Cloud build script