Quickstart with Passkey recovery
Build a functional authentication and wallet sample app using the Openfort React Native SDK with biometric passkey recovery.
This guide assumes you've completed the Getting Started guide and have your OpenfortProvider configured.
In this guide, we are going to help you set up your wallet recovery method to our Passkey method.
These are our wallet recovery methods you can choose from:
- Switch to Automatic Recovery
- Switch to Password Recovery
- Using Passkey Recovery
Not sure what wallet recovery method you need? Don't miss our guide.
Get your Openfort keys
From this point on, you'll need an Openfort account to handle project keys and settings.
In the Openfort Dashboard, select your desired app and navigate to the API keys. There, you'll get the following keys:
Project keys: Required for any wallet.
| Key | Exposure | Description |
|---|---|---|
| Publishable key | Public (client) | Initialize the SDK (in OpenfortProvider). |
| Secret key | Secret (server) | Privileged backend actions (sessions, wallet management). Never bundle in the app or commit to git. |
Shield Keys: Required for non-custodial wallets.
| Key | Exposure | Description |
|---|---|---|
| Publishable key | Public (client) | Public key the app uses to enable non‑custodial wallet features. Safe to include in client code. |
Get the Project Publishable Key and the Shield Publishable Key and save them for the configuration steps below.
Install passkey dependencies
Install the required passkey library:
npm install react-native-passkeysThen run prebuild to generate the native code:
npx expo prebuild --cleanSet up domain verification
Passkeys require HTTPS domain verification. For local development, use ngrok or a similar tunneling service.
Start ngrok tunnel
# In a separate terminal
ngrok http 8081Save the ngrok URL (e.g., abc123.ngrok-free.app) — this will be your Relying Party ID (RP ID).
Configure environment variables
# .env
# Publishable keys
OPENFORT_PROJECT_PUBLISHABLE_KEY=YOUR_PROJECT_PUBLISHABLE_KEY
OPENFORT_SHIELD_PUBLISHABLE_KEY=YOUR_SHIELD_PUBLISHABLE_KEY
# Passkey configuration
PASSKEY_RP_ID=abc123.ngrok-free.app
PASSKEY_RP_NAME=Your App NamePlatform setup
Choose your target platform and follow the setup instructions.
iOS uses Associated Domains to verify your app can use passkeys for a domain.
1. Get your Apple credentials
| Credential | Where to find it |
|---|---|
| Team ID | Apple Developer Portal → Membership |
| Bundle ID | Your app's bundle identifier (e.g., com.yourcompany.yourapp) |
2. Add environment variables
# .env (add to existing)
APPLE_TEAM_ID=YOUR_APPLE_TEAM_ID
APPLE_BUNDLE_ID=com.yourcompany.yourapp3. Configure app.config.js
// app.config.js
export default {
expo: {
name: "openfort-passkey-sample",
slug: "openfort-passkey-sample",
version: "1.0.0",
platforms: ["ios"],
ios: {
bundleIdentifier: process.env.APPLE_BUNDLE_ID,
associatedDomains: [
`webcredentials:${process.env.PASSKEY_RP_ID}`
],
},
extra: {
openfortPublishableKey: process.env.OPENFORT_PROJECT_PUBLISHABLE_KEY,
openfortShieldPublishableKey: process.env.OPENFORT_SHIELD_PUBLISHABLE_KEY,
passkeyRpId: process.env.PASSKEY_RP_ID,
passkeyRpName: process.env.PASSKEY_RP_NAME || "Openfort Wallet",
},
},
};4. Create the Apple App Site Association endpoint
Create an API route to serve the domain verification file:
// app/.well-known/apple-app-site-association+api.ts
export function GET() {
const teamId = process.env.APPLE_TEAM_ID;
const bundleId = process.env.APPLE_BUNDLE_ID;
if (!teamId || !bundleId) {
return Response.json({ error: "Missing Apple credentials" }, { status: 500 });
}
return Response.json({
webcredentials: {
apps: [`${teamId}.${bundleId}`],
},
}, {
headers: { "Content-Type": "application/json" },
});
}5. Verify the endpoint
curl https://your-subdomain.ngrok-free.app/.well-known/apple-app-site-associationExpected response:
{
"webcredentials": {
"apps": ["TEAM_ID.com.yourcompany.yourapp"]
}
}Android uses Digital Asset Links to verify your app can use passkeys for a domain.
1. Get your SHA256 fingerprint
Run this command to get your debug keystore fingerprint:
keytool -list -v -keystore ~/.android/debug.keystore \
-alias androiddebugkey -storepass android | grep SHA256For production, use your release keystore fingerprint.
2. Add environment variables
# .env (add to existing)
ANDROID_PACKAGE_NAME=com.yourcompany.yourapp
ANDROID_SHA256_FINGERPRINTS=FA:C6:17:45:DC:09:03:78:...3. Configure app.config.js
// app.config.js
export default {
expo: {
name: "openfort-passkey-sample",
slug: "openfort-passkey-sample",
version: "1.0.0",
platforms: ["android"],
android: {
package: process.env.ANDROID_PACKAGE_NAME,
},
extra: {
openfortPublishableKey: process.env.OPENFORT_PROJECT_PUBLISHABLE_KEY,
openfortShieldPublishableKey: process.env.OPENFORT_SHIELD_PUBLISHABLE_KEY,
passkeyRpId: process.env.PASSKEY_RP_ID,
passkeyRpName: process.env.PASSKEY_RP_NAME || "Openfort Wallet",
},
},
};4. Create the Digital Asset Links endpoint
Create an API route to serve the domain verification file:
// app/.well-known/assetlinks.json+api.ts
export function GET() {
const packageName = process.env.ANDROID_PACKAGE_NAME;
const sha256Fingerprints = process.env.ANDROID_SHA256_FINGERPRINTS;
if (!packageName || !sha256Fingerprints) {
return Response.json({ error: "Missing Android credentials" }, { status: 500 });
}
const fingerprints = sha256Fingerprints.split(",").map((f) => f.trim());
return Response.json([
{
relation: [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
target: {
namespace: "android_app",
package_name: packageName,
sha256_cert_fingerprints: fingerprints,
},
},
], {
headers: { "Content-Type": "application/json" },
});
}5. Verify the endpoint
curl https://your-subdomain.ngrok-free.app/.well-known/assetlinks.jsonExpected response:
[{
"relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": ["FA:C6:17:45:..."]
}
}]Configure OpenfortProvider with passkey
Replace your basic provider setup with passkey configuration:
// app/_layout.tsx
import { OpenfortProvider, RecoveryMethod } from "@openfort/react-native";
import Constants from "expo-constants";
import { Stack } from "expo-router";
const {
openfortPublishableKey,
openfortShieldPublishableKey,
passkeyRpId,
passkeyRpName,
} = Constants.expoConfig?.extra ?? {};
export default function RootLayout() {
return (
<OpenfortProvider
publishableKey={openfortPublishableKey}
walletConfig={{
debug: false,
recoveryMethod: RecoveryMethod.PASSKEY,
shieldPublishableKey: openfortShieldPublishableKey,
passkeyRpId,
passkeyRpName,
}}
verbose={true}
supportedChains={[
{
id: 84532,
name: 'Base Sepolia',
nativeCurrency: {
name: 'Base Sepolia Ether',
symbol: 'ETH',
decimals: 18
},
rpcUrls: { default: { http: ['https://sepolia.base.org'] } },
},
]}
>
<Stack>
<Stack.Screen name="index" />
</Stack>
</OpenfortProvider>
);
}Create the login and user screens
Create components that support passkey wallet creation and recovery:
// components/LoginScreen.tsx
import { OAuthProvider, useGuestAuth, useOAuth, usePasskeySupport } from "@openfort/react-native";
import { useEffect } from "react";
import { Button, Text, View, StyleSheet, ActivityIndicator } from "react-native";
export default function LoginScreen() {
const { signUpGuest } = useGuestAuth();
const { initOAuth, error: authError } = useOAuth();
const { isSupported: passkeySupported, isLoading: passkeyLoading } = usePasskeySupport();
useEffect(() => {
if (authError) {
console.error("[Openfort RN] Error logging in with OAuth:", authError);
}
}, [authError]);
if (passkeyLoading) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" />
<Text>Checking passkey support...</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Openfort Passkey Example</Text>
{passkeySupported ? (
<Text style={styles.supportText}>Passkeys are supported on this device</Text>
) : (
<Text style={styles.warningText}>Passkeys are not supported on this device</Text>
)}
<Button title="Login as Guest" onPress={() => signUpGuest()} />
<View style={styles.providersContainer}>
{[OAuthProvider.Google, OAuthProvider.Apple].map((provider) => (
<View key={provider.toString()}>
<Button
title={`Login with ${provider.toString()}`}
onPress={() => initOAuth({ provider })}
/>
</View>
))}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
gap: 10,
marginHorizontal: 10,
},
title: {
fontSize: 20,
fontWeight: "bold",
},
supportText: {
color: "green",
marginBottom: 10,
},
warningText: {
color: "orange",
marginBottom: 10,
},
providersContainer: {
display: "flex",
flexDirection: "column",
gap: 5,
margin: 10,
},
});Create your main app logic
Wire everything together:
// app/index.tsx
import LoginScreen from '../components/LoginScreen';
import { UserScreen } from '../components/UserScreen';
import { useUser } from '@openfort/react-native';
export default function Index() {
const { user } = useUser();
return !user ? <LoginScreen /> : <UserScreen />;
}Build and run your app
Build and run on a physical device (passkeys require hardware security):
# Rebuild native code
npx expo prebuild --cleannpm run iosNext steps
- Add gas sponsorship policies for gasless transactions
- Explore advanced wallet features in the React Native documentation
- View our React Native sample app with passkey support: