Using @capgo/capacitor-passkey
@capgo/capacitor-passkey lets a Capacitor app keep the same WebAuthn flow you already use on the web:
await navigator.credentials.create({ publicKey: registrationOptions });
await navigator.credentials.get({ publicKey: requestOptions });
On native builds, the plugin installs a shim for navigator.credentials.create() and navigator.credentials.get(), forwards the request to iOS and Android passkey APIs, and returns browser-like credential objects to your app.
Install the plugin
bun add @capgo/capacitor-passkey
bunx cap sync
Configure the host app once
Add the plugin config in capacitor.config.ts or capacitor.config.json:
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'app.capgo.passkey.example',
appName: 'My App',
webDir: 'dist',
plugins: {
CapacitorPasskey: {
origin: 'https://signin.example.com',
autoShim: true,
domains: ['signin.example.com'],
},
},
};
export default config;
Run sync again after changing the config:
bunx cap sync
During sync, the plugin patches the generated native host projects:
- iOS: associated domains entitlements
- Android:
asset_statementsmetadata for Digital Asset Links
Import the shim once
Import the auto entrypoint in your app bootstrap:
import '@capgo/capacitor-passkey/auto';
After that, your existing browser-style passkey code can stay the same.
Keep your normal WebAuthn flow
const credential = await navigator.credentials.create({
publicKey: registrationOptions,
});
const assertion = await navigator.credentials.get({
publicKey: requestOptions,
});
Native setup still needs website trust files
The plugin reduces app-side work, but passkeys still depend on the website trust files for your relying-party domain:
- iOS needs
/.well-known/apple-app-site-association - Android needs
/.well-known/assetlinks.json
The detailed setup is documented here:
Important Android caveat
Android Credential Manager can share the same relying party and passkeys as your website when Digital Asset Links are configured, but the native assertion origin is not identical to a browser origin. If your backend strictly validates clientDataJSON.origin, make sure it accepts the Android app origin alongside your website origin.