git:20260501.ed3e92e to git:20261011.86834d4

96 added, 427 removed. Audit A to A.

---
name: capacitor-best-practices
- description: Best practices for Capacitor app development including project structure, plugin usage, performance optimization, security, and deployment. Use this skill when reviewing Capacitor code, setting up new projects, or optimizing existing apps.
+ description: Review and set up Capacitor apps against current best practices (Capacitor 8.5 stable, 9 alpha via `@next`). Use for a Capacitor codebase review, `capacitor.config.ts` audit, new project setup, aligning `@capacitor/core`/`cli`/`ios`/`android` and plugin majors, plugin patterns (availability checks, `checkPermissions`/`requestPermissions`, error `code` handling, web fallbacks), or release prep; or on `"<Plugin>" plugin is not implemented on ios`, `Could not find the web assets directory`, `platform has not been added yet`, a dev `server.url` shipped to production, lost edits in `android/app/src/main/assets/public`, SPM vs CocoaPods, or AppDelegate/UIScene drift. Do not use for security audits (capacitor-security), performance (capacitor-performance), tests (capacitor-testing), crashes (debugging-capacitor), major upgrades (capacitor-app-upgrades), SPM migration (cocoapods-to-spm), UIScene adoption (capacitor-uiscene-migration), or live updates (capgo-live-updates).
---
# Capacitor Best Practices
- Comprehensive guidelines for building production-ready Capacitor applications.
-
- ## When to Use This Skill
-
- - Setting up a new Capacitor project
- - Reviewing Capacitor app architecture
- - Optimizing app performance
- - Implementing security measures
- - Preparing for app store submission
-
- ## Project Structure
-
- ### Recommended Directory Layout
+ Review a Capacitor app for structural mistakes that cause broken builds, lost edits, production leaks, and plugin crashes. Route deep topics to the sibling skill that owns them.
- ```text
- my-app/
- ├── src/ # Web app source
- ├── android/ # Android native project
- ├── ios/ # iOS native project
- ├── capacitor.config.ts # Capacitor configuration
- ├── package.json
- └── tsconfig.json
- ```
+ ## When to Use
- ### Configuration Best Practices
+ TRIGGER when:
+ - The user asks for a general code/architecture review of a Capacitor or Ionic + Capacitor app.
+ - Setting up a new Capacitor project or adding iOS/Android to an existing web app.
+ - `capacitor.config.ts|json` review: `webDir`, `server.url`, `cleartext`, plugin config, env-specific config.
+ - Version drift between `@capacitor/core`, `@capacitor/cli`, `@capacitor/ios`, `@capacitor/android`, or plugin majors that do not match Capacitor's major.
+ - Plugin usage review: missing `npx cap sync`, no availability check, no permission flow, matching on error message strings.
+ - Errors such as `"<Plugin>" plugin is not implemented on ios`, `Could not find the web assets directory`, `ios platform has not been added yet`.
+ - Pre-release sanity checklist across config, native projects, and live updates.
- **capacitor.config.ts** (CORRECT):
- ```typescript
- import type { CapacitorConfig } from '@capacitor/cli';
+ Do not use:
+ - Security audit, secrets, storage, pinning, CSP -> `capacitor-security`; Xcode hardening -> `capacitor-ios-security-hardening`.
+ - Bundle size, startup time, memory, bridge profiling -> `capacitor-performance`.
+ - Unit/E2E test setup and plugin mocking -> `capacitor-testing`.
+ - Crashes, white screen, native logs -> `debugging-capacitor`, `ios-android-logs`.
+ - Major upgrades -> `capacitor-app-upgrades` (8 -> 9: `capacitor-app-upgrade-v8-to-v9`).
+ - CocoaPods -> SPM -> `cocoapods-to-spm`; UIScene adoption -> `capacitor-uiscene-migration`.
+ - Finding or installing a specific plugin -> `capacitor-plugins`.
+ - Store submission and review -> `capacitor-app-store`, `capacitor-apple-review-preflight`; CI -> `capacitor-ci-cd`; live updates -> `capgo-live-updates`.
- const config: CapacitorConfig = {
- appId: 'com.company.app',
- appName: 'My App',
- webDir: 'dist',
- server: {
- // Only enable for development
- ...(process.env.NODE_ENV === 'development' && {
- url: 'http://localhost:5173',
- cleartext: true,
- }),
- },
- plugins: {
- SplashScreen: {
- launchAutoHide: false,
- },
- },
- };
+ ## Current baseline (October 2026)
- export default config;
- ```
+ | | Capacitor 8 (stable, `latest` 8.5.x) | Capacitor 9 (`next`, alpha) |
+ |---|---|---|
+ | Node | 22+ | 24+ |
+ | iOS min / Xcode | iOS 15 / Xcode 26 (8.5 adopts UIScene so Xcode 27 builds work) | iOS 16 / Xcode 27+ |
+ | Android | minSdk 24, compile/target 36, AGP 8.13, Gradle 8.14.3 | minSdk 26, compile/target 37, AGP 9.2.1, Gradle 9.5.1 |
+ | iOS deps | SPM default for new projects | SPM default |
- **capacitor.config.json** (AVOID):
- ```json
- {
- "server": {
- "url": "http://localhost:5173",
- "cleartext": true
- }
- }
- ```
- *Never commit development server URLs to production*
+ CocoaPods Trunk is expected to go read-only on December 2, 2026. Recommend SPM for every app still on CocoaPods. Install Capacitor 9 only with `@next` until it reaches GA, and do not recommend it for production apps.
- ## Plugin Usage
+ ## Review workflow
- ### CRITICAL: Always Use Latest Capacitor
+ 1. **Inventory (read-only).**
+ ```bash
+ npx cap doctor # installed vs latest Capacitor packages
+ npx cap ls # plugins Capacitor detects per platform
+ cat capacitor.config.ts 2>/dev/null || cat capacitor.config.json
+ ls ios/App/Podfile ios/App/CapApp-SPM 2>/dev/null # CocoaPods or SPM
+ grep -nE "minSdkVersion|compileSdkVersion|targetSdkVersion" android/variables.gradle
+ ```
+ 2. **For large codebases, write a TODO list** of areas (config, versions, plugins, native iOS, native Android, live updates, release) and review one at a time.
+ 3. **Report findings before editing**, grouped by severity, each with file:line and the fix. Ask before invasive changes (upgrading a major, migrating to SPM, enabling R8, changing `appId`).
+ 4. **Fix**, loading the matching reference.
+ 5. **Run Verification.**
- Keep Capacitor core packages in sync:
+ ## Rules that matter most
- ```bash
- npm install @capacitor/core@latest @capacitor/cli@latest
- npm install @capacitor/ios@latest @capacitor/android@latest
- npx cap sync
- ```
+ 1. **Keep Capacitor packages on one version.** `@capacitor/core`, `cli`, `ios`, `android` must match. Plugin majors must match the Capacitor major (official and `@capgo/*` plugins follow it: v8 plugins for Capacitor 8).
+ 2. **`npx cap sync` after every plugin install/removal and every config change.** Missing sync produces `"<Plugin>" plugin is not implemented on <platform>`.
+ 3. **Never edit generated web assets.** `android/app/src/main/assets/public/` and `ios/App/App/public/` are overwritten by `cap copy`/`sync`. Edit `src/` and rebuild.
+ 4. **Commit `ios/` and `android/`.** They are source code with your native customizations, not build output. Do commit `capacitor.config.*`; do not commit `node_modules`, `Pods/`, build dirs.
+ 5. **No dev settings in release config.** `server.url`, `server.cleartext`, `allowNavigation` to dev hosts, `webContentsDebuggingEnabled: true`. Gate them on an env var (see references/config-and-project.md). On Capacitor 9, use `npx cap run <platform> --url <dev-url>` for live reload.
+ 6. **Feature-detect before calling plugins.** `Capacitor.isNativePlatform()`, `Capacitor.getPlatform()`, `Capacitor.isPluginAvailable('Name')`. Provide a web fallback or hide the feature.
+ 7. **Use the permission API.** Call `checkPermissions()` then `requestPermissions()` for official plugins that expose them, and handle `denied` with a path to Settings. Add the matching `NS*UsageDescription` keys / Android permissions.
+ 8. **Branch on `error.code`, not `error.message`.** Messages change between versions and platforms. Official plugins expose structured codes (for example `OS-PLUG-CAMR-0006` for a cancelled camera capture in `@capacitor/camera` 8.1+ new APIs); Capacitor core uses `UNIMPLEMENTED` / `UNAVAILABLE`.
+ 9. **Move large binary data as files, not base64.** Pass file paths/URIs across the bridge and render with `Capacitor.convertFileSrc()`.
+ 10. **iOS lifecycle is scene-based from Capacitor 8.5.** After adopting UIScene, AppDelegate `application(_:open:options:)`, `continue userActivity`, and foreground/background callbacks stop firing. Move custom native code to `SceneDelegate` (`capacitor-uiscene-migration`). Avoid `UIScreen.main` and idiom-based layout for resizable windows.
+ 11. **Live updates: call `CapacitorUpdater.notifyAppReady()` on every launch**, and apply manual updates with `next()` (on background/relaunch) rather than `set()` mid-session. Details in `capgo-live-updates`.
+ 12. **Secrets never go in the app.** Anything in the bundle or `capacitor.config` is extractable. Hand off to `capacitor-security` for anything beyond this rule.
- ### Plugin Installation Pattern
+ ## References
- **CORRECT**:
- ```bash
- # 1. Install the package
- npm install @capgo/capacitor-native-biometric
+ Only load a reference when its topic is in play.
- # 2. Sync native projects
- npx cap sync
+ | Reference | Load when |
+ |-----------|-----------|
+ | [references/config-and-project.md](references/config-and-project.md) | Reviewing `capacitor.config`, env-specific builds, repo layout, `.gitignore`, version alignment |
+ | [references/plugins.md](references/plugins.md) | Plugin install/sync, availability checks, permissions, error codes, web fallbacks, lazy loading, mocking |
+ | [references/native-projects.md](references/native-projects.md) | iOS SPM vs CocoaPods, UIScene, deployment targets; Android `variables.gradle`, R8, Capacitor 9 Gradle changes |
+ | [references/release-checklist.md](references/release-checklist.md) | Preparing a release build or reviewing release readiness |
- # 3. For iOS: Install pods (or use SPM)
- cd ios/App && pod install && cd ../..
- ```
+ ## Verification
- **INCORRECT**:
```bash
- # Missing sync step
- npm install @capgo/capacitor-native-biometric
- # App crashes because native code not linked
- ```
-
- ### Plugin Initialization
-
- **CORRECT** - Check availability before use:
- ```typescript
- import { NativeBiometric, BiometryType } from '@capgo/capacitor-native-biometric';
-
- async function authenticate() {
- const { isAvailable, biometryType } = await NativeBiometric.isAvailable();
-
- if (!isAvailable) {
- // Fallback to password
- return authenticateWithPassword();
- }
-
- try {
- await NativeBiometric.verifyIdentity({
- reason: 'Authenticate to access your account',
- title: 'Biometric Login',
- });
- return true;
- } catch (error) {
- // User cancelled or biometric failed
- return false;
- }
- }
- ```
-
- **INCORRECT** - No availability check:
- ```typescript
- // Will crash if biometrics not available
- await NativeBiometric.verifyIdentity({ reason: 'Login' });
- ```
-
- ## Performance Optimization
-
- ### CRITICAL: Lazy Load Plugins
-
- **CORRECT** - Dynamic imports:
- ```typescript
- // Only load when needed
- async function scanDocument() {
- const { DocumentScanner } = await import('@capgo/capacitor-document-scanner');
- return DocumentScanner.scanDocument();
- }
- ```
-
- **INCORRECT** - Import everything at startup:
- ```typescript
- // Increases initial bundle size
- import { DocumentScanner } from '@capgo/capacitor-document-scanner';
- import { NativeBiometric } from '@capgo/capacitor-native-biometric';
- import { Camera } from '@capacitor/camera';
- // ... 20 more plugins
- ```
-
- ### HIGH: Optimize WebView Performance
-
- **CORRECT** - Use hardware acceleration:
- ```xml
- <!-- android/app/src/main/AndroidManifest.xml -->
- <application
- android:hardwareAccelerated="true"
- android:largeHeap="true">
- ```
-
- ```xml
- <!-- ios/App/App/Info.plist -->
- <key>UIViewGroupOpacity</key>
- <false/>
- ```
-
- ### HIGH: Minimize Bridge Calls
-
- **CORRECT** - Batch operations:
- ```typescript
- // Single call with batch data
- await Storage.set({
- key: 'userData',
- value: JSON.stringify({ name, email, preferences }),
- });
- ```
-
- **INCORRECT** - Multiple bridge calls:
- ```typescript
- // Each call crosses the JS-native bridge
- await Storage.set({ key: 'name', value: name });
- await Storage.set({ key: 'email', value: email });
- await Storage.set({ key: 'preferences', value: JSON.stringify(preferences) });
- ```
-
- ### MEDIUM: Image Optimization
-
- **CORRECT**:
- ```typescript
- import { Camera, CameraResultType } from '@capacitor/camera';
-
- const photo = await Camera.getPhoto({
- quality: 80, // Not 100
- width: 1024, // Reasonable max
- resultType: CameraResultType.Uri, // Not Base64 for large images
- correctOrientation: true,
- });
- ```
-
- **INCORRECT**:
- ```typescript
- const photo = await Camera.getPhoto({
- quality: 100,
- resultType: CameraResultType.Base64, // Memory intensive
- // No size limits
- });
- ```
-
- ## Security Best Practices
-
- ### CRITICAL: Secure Storage
-
- **CORRECT** - Use secure storage for sensitive data:
- ```typescript
- import { NativeBiometric } from '@capgo/capacitor-native-biometric';
-
- // Store credentials securely
- await NativeBiometric.setCredentials({
- username: 'user@example.com',
- password: 'secret',
- server: 'api.myapp.com',
- });
-
- // Retrieve with biometric verification
- const credentials = await NativeBiometric.getCredentials({
- server: 'api.myapp.com',
- });
- ```
-
- **INCORRECT** - Plain storage:
- ```typescript
- import { Preferences } from '@capacitor/preferences';
-
- // NEVER store sensitive data in plain preferences
- await Preferences.set({
- key: 'password',
- value: 'secret', // Stored in plain text!
- });
- ```
-
- ### CRITICAL: Certificate Pinning
-
- For production apps handling sensitive data:
-
- ```typescript
- // capacitor.config.ts
- const config: CapacitorConfig = {
- plugins: {
- CapacitorHttp: {
- enabled: true,
- },
- },
- server: {
- // Disable cleartext in production
- cleartext: false,
- },
- };
- ```
-
- ### HIGH: Root/Jailbreak Detection
-
- ```typescript
- import { IsRoot } from '@capgo/capacitor-is-root';
-
- async function checkDeviceSecurity() {
- const { isRooted } = await IsRoot.isRooted();
-
- if (isRooted) {
- // Show warning or restrict functionality
- showSecurityWarning('Device appears to be rooted/jailbroken');
- }
- }
- ```
-
- ### HIGH: App Tracking Transparency (iOS)
-
- ```typescript
- import { AppTrackingTransparency } from '@capgo/capacitor-app-tracking-transparency';
-
- async function requestTracking() {
- const { status } = await AppTrackingTransparency.requestPermission();
-
- if (status === 'authorized') {
- // Enable analytics
- }
- }
- ```
-
- ## Error Handling
-
- ### CRITICAL: Always Handle Plugin Errors
-
- **CORRECT**:
- ```typescript
- import { Camera, CameraResultType } from '@capacitor/camera';
-
- async function takePhoto() {
- try {
- const image = await Camera.getPhoto({
- quality: 90,
- resultType: CameraResultType.Uri,
- });
- return image;
- } catch (error) {
- if (error.message === 'User cancelled photos app') {
- // User cancelled, not an error
- return null;
- }
- if (error.message.includes('permission')) {
- // Permission denied
- showPermissionDialog();
- return null;
- }
- // Unexpected error
- console.error('Camera error:', error);
- throw error;
- }
- }
- ```
-
- **INCORRECT**:
- ```typescript
- // No error handling
- const image = await Camera.getPhoto({ quality: 90 });
- ```
-
- ## Live Updates
-
- ### Using Capacitor Updater
-
- ```typescript
- import { CapacitorUpdater } from '@capgo/capacitor-updater';
-
- // Notify when app is ready
- CapacitorUpdater.notifyAppReady();
-
- // Listen for updates
- CapacitorUpdater.addListener('updateAvailable', async (update) => {
- // Download in background
- const bundle = await CapacitorUpdater.download({
- url: update.url,
- version: update.version,
- });
-
- // Apply on next app start
- await CapacitorUpdater.set(bundle);
- });
- ```
-
- ### Update Strategy
-
- **CORRECT** - Background download, apply on restart:
- ```typescript
- // Download silently
- const bundle = await CapacitorUpdater.download({ url, version });
-
- // User continues using app...
-
- // Apply when they close/reopen
- await CapacitorUpdater.set(bundle);
- ```
-
- **INCORRECT** - Interrupt user:
- ```typescript
- // Don't force reload while user is active
- const bundle = await CapacitorUpdater.download({ url, version });
- await CapacitorUpdater.reload(); // Disrupts user
- ```
-
- ## Native Project Management
-
- ### iOS: Use Swift Package Manager (SPM)
-
- Modern approach - prefer SPM over CocoaPods:
-
- ```ruby
- # Podfile - Remove plugin pods, use SPM instead
- target 'App' do
- capacitor_pods
- # Plugin dependencies via SPM in Xcode
- end
- ```
-
- ### Android: Gradle Configuration
+ # Versions aligned (all @capacitor/* core packages show the same version)
+ npm ls @capacitor/core @capacitor/cli @capacitor/ios @capacitor/android
+ npx cap doctor
- ```groovy
- // android/app/build.gradle
- android {
- defaultConfig {
- minSdkVersion 22
- targetSdkVersion 34
- }
+ # Web build exists where webDir points, then sync succeeds without errors
+ npm run build && npx cap sync
- buildTypes {
- release {
- minifyEnabled true
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
- }
- ```
+ # No dev server or debug flags in the synced native config
+ grep -nE "\"url\"|cleartext|webContentsDebuggingEnabled" \
+ android/app/src/main/assets/capacitor.config.json ios/App/App/capacitor.config.json
- ## Testing
+ # No hand edits in generated web assets (should show nothing)
+ git status --porcelain android/app/src/main/assets/public ios/App/App/public
- ### Plugin Mocking
+ # Error handling does not depend on message text
+ grep -rnE "error\.message\s*(===|==|\.includes)" src/
- ```typescript
- // Mock for web testing
- jest.mock('@capgo/capacitor-native-biometric', () => ({
- NativeBiometric: {
- isAvailable: jest.fn().mockResolvedValue({
- isAvailable: true,
- biometryType: 'touchId',
- }),
- verifyIdentity: jest.fn().mockResolvedValue({}),
- },
- }));
+ # Native builds
+ cd android && ./gradlew assembleRelease && cd ..
+ xcodebuild -project ios/App/App.xcodeproj -scheme App -configuration Release -destination 'generic/platform=iOS' build CODE_SIGNING_ALLOWED=NO
```
- ### Platform Detection
-
- ```typescript
- import { Capacitor } from '@capacitor/core';
-
- if (Capacitor.isNativePlatform()) {
- // Native-specific code
- } else {
- // Web fallback
- }
-
- // Or check specific platform
- if (Capacitor.getPlatform() === 'ios') {
- // iOS-specific code
- }
- ```
+ With CocoaPods, build `ios/App/App.xcworkspace` with `-workspace` instead of `-project`. Finally run on a real device and exercise each plugin-backed feature, including permission denial.
- ## Deployment Checklist
+ ## Error handling
- - [ ] Remove development server URLs from config
- - [ ] Enable ProGuard for Android release builds
- - [ ] Set appropriate iOS deployment target
- - [ ] Test on real devices, not just simulators
- - [ ] Verify all permissions are declared
- - [ ] Test with poor network conditions
- - [ ] Verify deep links work correctly
- - [ ] Test app backgrounding/foregrounding
- - [ ] Verify push notifications work
- - [ ] Test biometric authentication edge cases
+ | Error | Cause | Fix |
+ |-------|-------|-----|
+ | `"<Plugin>" plugin is not implemented on ios` (or `android`) | Plugin installed but not synced, plugin has no implementation for that platform, or plugin major does not match Capacitor | `npx cap sync`; check `npx cap ls`; install the plugin version matching your Capacitor major; rebuild the native app |
+ | `"<Plugin>.<method>()" is not implemented on web` | Calling a native-only method in the browser | Guard with `Capacitor.isNativePlatform()` or `isPluginAvailable`, provide a web fallback |
+ | `Could not find the web assets directory: ./dist.` | `webDir` wrong or web app not built | Build first; set `webDir` to the real output folder (`dist`, `www`, `build`, `out`) |
+ | `ios platform has not been added yet.` | Native project missing | `npx cap add ios` (SPM default) or restore the committed `ios/` folder |
+ | `Invalid App ID "<id>"` | `appId` not reverse-domain or contains invalid characters | Use `com.company.app` style; changing it on a published app creates a new store listing |
+ | App loads a LAN IP or blank screen in production | `server.url` shipped from a dev config | Remove it, rebuild, `npx cap sync`, rebuild the native app |
+ | `net::ERR_CLEARTEXT_NOT_PERMITTED` | HTTP request on Android | Use HTTPS; `cleartext` only in dev config (see `capacitor-security`) |
+ | `pod install` fails / CocoaPods spec errors | CocoaPods trunk deprecation, outdated specs | Migrate with `cocoapods-to-spm` |
+ | Deep links or `resume` handling stopped on iOS after 8.5 | Custom code still in AppDelegate after UIScene adoption | Move it to `SceneDelegate` (`capacitor-uiscene-migration`) |
## Resources
- - Capacitor Documentation: https://capacitorjs.com/docs
- - Capgo Documentation: https://capgo.app/docs
- - Ionic Framework: https://ionicframework.com/docs
+ - Capacitor docs: https://capacitorjs.com/docs
+ - Config reference: https://capacitorjs.com/docs/config
+ - Environment-specific configurations: https://capacitorjs.com/docs/guides/environment-specific-configurations
+ - Updating to 8.5 (UIScene): https://capacitorjs.com/docs/updating/8-5
+ - Capgo docs: https://capgo.app/docs/