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 forwardsPluginCalls from the WebView to native handlers.PluginManager.java– discovers, loads, and invokes plugins listed inPluginConfig.java.CapacitorWebView.java– a custom WebView that injects the JavaScript bridge (native-bridge.jsinandroid/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
- Entry point –
cli/src/index.tsis the first executable; it parses CLI args, readscli/package.jsonfor dependencies, then invokes the appropriate sub‑command (add,run,build). - Platform addition –
cli/src/android/add.ts(and the iOS counterpart) callsnpx cap add android, which creates the Android project skeleton (android/capacitor/), adds Gradle wiring (android/capacitor/build.gradle), and injects the bridge Java files. - Runtime bootstrap – When the web app loads,
native-bridge.jsestablishes awindow.Capacitorobject that forwards calls to the native side viawindow.capacitor.runMethod(...). On Android, this lands inBridgeWebViewClient.java, which hands the message toBridge.java. - Plugin invocation –
PluginManager.javaresolves the method name, extracts arguments from thePluginCall, 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.gradledeclares dependencies without apackage-lock.json/yarn lockfile, making builds non‑reproducible across environments. Adding a lockfile (or usingpnpmwith apnpm-lockfile) is recommended. - Test coverage – 79 test files exist across
android/capacitor/src/test/andcli/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.