Expo/React Native drop-in replacement for the Better Auth passkeyClient that works everywhere Better Auth runs today: Web, Android, and iOS. macOS shares the same native implementation but still needs wider community testing—pull requests and reports are welcome.
- Drop-in client: swap
passkeyClient()withexpoPasskeyClient()and keep the exact same Better Auth API surface. - Native Credential APIs: wraps WebAuthn calls with
ASAuthorizationControlleron Apple platforms and Android Credential Manager on Android. - Works with managed or bare Expo projects; no ejecting required.
- Single code path for web builds—falls back to the stock Better Auth web client when
Platform.OS === 'web'. - TypeScript-first with strict types mirrored from
@simplewebauthn/types.
| Platform | Status | Notes |
|---|---|---|
| Web | ✅ | Uses Better Auth's default WebAuthn client |
| iOS 15.1+ | ✅ | Uses ASAuthorizationPlatformPublicKeyCredentialProvider |
| Android (Credential Manager) | ✅ | Requires Google Play Services 23.30+ |
| macOS 12+ | Same native code path as iOS; please file issues/PRs |
- A Better Auth server configured with the
passkeyplugin. Make sure the server runs on HTTPS with a hostname that matches the relying party ID (rpID). - Expo SDK 49 or newer (tested with 54). For React Native CLI users, Expo Modules Autolinking must be set up.
nanostoresavailable in your app (Better Auth already depends on it).
npm install @lobehub/expo-better-auth-passkey
# or
yarn add @lobehub/expo-better-auth-passkey
# or
bun add @lobehub/expo-better-auth-passkeyThe native module is autolinked. If you use a bare/React Native CLI project, run npx pod-install after installing.
Replace the standard passkeyClient with expoPasskeyClient. Nothing else changes:
import { createAuthClient } from 'better-auth/react'
import { expoPasskeyClient } from '@lobehub/expo-better-auth-passkey'
export const authClient = createAuthClient({
baseURL: 'https://your-api.mydomain.com',
plugins: [
expoPasskeyClient(),
// ...the rest of your Better Auth client plugins
],
})
// Works exactly like Better Auth's stock client:
await authClient.passkey.addPasskey({ name: 'My iPhone' })
await authClient.signIn.passkey({ email: 'user@example.com' })The module internally forwards every server call to Better Auth and only overrides the WebAuthn credential creation/retrieval steps. Web builds automatically fall back to the original Better Auth WebAuthn implementation.
- Better Auth passkey plugin: Configure
rpID,rpName, andoriginto match the public domain your app will use. When you ship Android builds, add anandroid:apk-key-hash:<BASE64_SHA256>entry for every signing certificate so Better Auth can validate APK-originated passkey requests. - Trusted origins: Include all app schemes you intend to use, e.g.
myapp://,https://localhost, and any Expo dev tunnels. Example:trustedOrigins: [ 'https://auth.example.com', 'myapp://', 'com.example.myapp://', ]
- HTTPS only: Passkeys require secure origins. Use a tunneling service (ngrok, localhost HTTPS) during development.
- Enable the Associated Domains capability in Xcode or via
expo prebuildconfig (ios.associatedDomains). - Add a
webcredentials:entry for every relying party domain:{ "expo": { "ios": { "associatedDomains": [ "webcredentials:auth.example.com" ] } } } - Host an
apple-app-site-associationfile athttps://auth.example.com/.well-known/apple-app-site-associationwith content similar to:{ "applinks": { "apps": [], "details": [] }, "webcredentials": { "apps": ["<TEAMID>.com.example.myapp"] } }- No file extension and served as
application/json(orapplication/pkcs7-mime). <TEAMID>is your Apple developer team ID; the bundle identifier must match your release build.
- No file extension and served as
- Make sure your
Info.plistallows the relying party hostname as an associated domain. Expo handles this automatically whenassociatedDomainsis set.
Optional hints supported by this module:
- Pass
{ useAutoRegister: true }toaddPasskeyto request the platform UI to suggest immediate passkey creation (iOS 16+). - Pass
{ autoFill: true }tosignIn.passkeyto allow autofill suggestions (iOS 16+).
- Min requirements: Android 9 (API 28) or newer with Credential Manager 1.3.0+. Users need Google Play Services 23.30+ for passkeys.
- App signing SHA-256: Obtain your app signing certificate fingerprint. For debug builds:
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android | grep 'SHA256:'
Replace this with your Play App Signing fingerprint for production. Convert each raw SHA-256 fingerprint to base64 and add `android:apk-key-hash:<BASE64_SHA256>` entries to the Better Auth `origin` array so the server trusts credentials coming from your APK.
5. **Optional**: If you want to forward your Android app's HTTPS origin when calling Credential Manager, request the `android.permission.CREDENTIAL_MANAGER_SET_ORIGIN` permission (API 34+). The module automatically falls back when the permission is missing, so you can skip it if you don't need per-domain attribution.
6. The Android bridge rewrites `user.displayName` to match `user.name` before presenting the system dialog so that each passkey nickname shows up without conflicting with the persistent Better Auth `displayName` field.
3. Host `https://auth.example.com/.well-known/assetlinks.json` with content:
```json
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints": [
"12:34:56:...:AB"
]
}
}
]
package_nameis your Android application ID.- Include every signing fingerprint you use (debug, release, Play signing).
- If you use Expo managed workflow, set
android.packageinapp.json/app.config.jsso autolinking matches the identifier above. - Ensure the relying party hostname (
rpID) exactly matches the host portion of your HTTPS domain (auth.example.com). The module automatically injects theoriginfield before returning to Better Auth.
No additional setup beyond the regular Better Auth client. The plugin detects the web platform and hands control back to Better Auth's built-in WebAuthn flow.
npm run build– compile the TypeScript sources.npm run lint– lint with Expo module preset.npm run test– run the Expo module test runner.cd example && npm install && npm start– launch the example app. Usenpm run ios/npm run androidfrom theexampledirectory for device simulators.
- User cancellations surface as Better Auth errors with code
AUTH_CANCELLEDso you can display friendly UI. - Native errors are logged to the console for debugging. Use a production logging service to capture them on devices.
- Android Credential Manager exceptions (
NO_ACTIVITY,CREATE_ERROR,GET_ERROR) bubble through the returned error object—inspectresult.errorwhen debugging.
macOS uses the same AuthenticationServices implementation as iOS but has limited coverage. If you can validate on macOS 12+, please open an issue or PR with results. Contributions for advanced features (cross-platform authenticators, passkey list management, web fallbacks) are encouraged.
- Fork the repo and install dependencies with
npm install. - Use
npm run buildbefore opening a PR to ensure the generatedbuild/output is up to date. - Follow the lint/test scripts above. Please include repro steps for any passkey edge cases you fix.
MIT © LobeHub