The Problem

Teams that want a single JavaScript/TypeScript codebase to target iOS, Android, and the web often wrestle with fragmenting native builds, managing platform‑specific plugins, and keeping native projects in sync with web changes. Capacitor addresses this by treating native projects as source artifacts rather than build artifacts and by providing a thin cross‑platform bridge that lets web code call native SDKs directly.

What This Does

Capacitor is a thin runtime that sits between a web app and the native platform. The repository’s core (core/src/index.ts) exports the public API (Capacitor, Plugn, etc.) that the CLI and consumer apps use. The CLI (cli/src/index.ts) bootstraps projects, adds platforms, and runs code‑generation scripts; it delegates plugin handling to cli/src/util/cli.ts.

On the native side, the Android module lives under android/capacitor/. Key files include:

  • Bridge.java – the central message hub that forwards PluginCalls from the WebView to native handlers.
  • PluginManager.java – discovers, loads, and invokes plugins listed in PluginConfig.java.
  • CapacitorWebView.java – a custom WebView that injects the JavaScript bridge (native-bridge.js in android/capacitor/src/main/assets).

iOS mirrors this structure in ios/ (Swift files, Xcode project) but the same architectural concepts apply. The plugin system is deliberately opt‑in: developers write a small Java/Kotlin (Android) or Swift (iOS) wrapper that implements the Plugin interface, then register it via CapacitorPlugin annotation. This keeps the web surface area small while still granting full native access.

How It Is Wired

  1. Entry point – cli/src/index.ts is the first executable; it parses CLI args, reads cli/package.json for dependencies, then invokes the appropriate sub‑command (add, run, build).
  2. Platform addition – cli/src/android/add.ts (and the iOS counterpart) calls npx cap add android, which creates the Android project skeleton (android/capacitor/), adds Gradle wiring (android/capacitor/build.gradle), and injects the bridge Java files.
  3. Runtime bootstrap – When the web app loads, native-bridge.js establishes a window.Capacitor object that forwards calls to the native side via window.capacitor.runMethod(...). On Android, this lands in BridgeWebViewClient.java, which hands the message to Bridge.java.
  4. Plugin invocation – PluginManager.java resolves the method name, extracts arguments from the PluginCall, and invokes the corresponding Kotlin/Java method. The result travels back through the same bridge to the web promise.

The call graph is therefore: CLI → platform scaffold → native bridge → PluginManager → plugin implementation. Changing a plugin requires edits to the native wrapper and the corresponding TypeScript declaration (usually in core/src/), but the web‑side API stays stable.

How To Use It

# 1️⃣ Clone the repo (use the exact URL provided)
git clone https://github.com/moses-y/capacitor

# 2️⃣ Install the core packages
cd capacitor
npm install @capacitor/core @capacitor/cli

# 3️⃣ Initialise a new Capacitor project (creates capacitor.config.json)
npx cap init

# 4️⃣ Add a native platform – this scaffolds the Android project
npm install @capacitor/android
npx cap add android
#   → creates android/capacitor/*, updates android/capacitor/build.gradle

# 5️⃣ (Optional) Add iOS
npm install @capacitor/ios
npx cap add ios

Configuration: capacitor.config.json (generated by npx cap init) holds app ID, server URL, and plugin whitelist. Environment‑specific keys (e.g., API endpoints) are typically stored in this JSON file or injected via Gradle/Android build.gradle variables.

Running: npx cap run android (or ios) launches the app on a device/emulator, bundling the web assets and attaching the native bridge.

Real‑World Use

A React web app that needs camera access can install the community plugin:

npm install @capacitor/camera
npx cap sync   # syncs plugin Java/Kotlin files

Then use the standard Capacitor API:

import { Camera, CameraResultType, Capacitor } from '@capacitor/core';

async takePhoto() {
  const image = await Camera.getPhoto({
    resultType: CameraResultType.Data,
    quality: 90,
  });
  // `image.data` is a base64 string usable in an <img> tag
  document.querySelector('img')!.src = `data:image/jpeg;base64,${image.data}`;
}

The plugin’s Android implementation lives in capacitor-cordova-android-plugins/ and follows the Plugin interface defined in android/capacitor/src/main/java/com/getcapacitor/plugin/Plugin.java.

Code Health & Issues

  • Lockfile risk – android-template/app/build.gradle declares dependencies without a package-lock.json/yarn lockfile, making builds non‑reproducible across environments. Adding a lockfile (or using pnpm with a pnpm-lockfile) is recommended.
  • Test coverage – 79 test files exist across android/capacitor/src/test/ and cli/test/. The core logic is exercised, but iOS‑specific unit tests are sparse (Swift files are present but test targets are limited).
  • Documentation – 15 doc files are present; the README and CONTRIBUTING guide are solid, but inline JSDoc for some core functions could be expanded.

The Bottom Line

Capacitor delivers a pragmatic path to native PWA deployment with a thin, well‑documented bridge and a plugin model that keeps web and native code decoupled. The architecture is transparent (CLI → bridge → PluginManager → plugin) and the codebase is large enough to be battle‑tested, yet small enough to audit. The main caution is the missing lockfile in the Android template, which can cause divergent builds; adding one and locking dependency versions will improve reproducibility. Teams already using React/TypeScript and comfortable with a small native wrapper will find Capacitor the least‑friction option for cross‑platform native distribution.