Authenticating Users via Passkey

This guide assumes you have already followed the Setup Guide and have set up the Alchemy Account Provider using this guide. Please refer to the guides above for more information on how to properly setup your project.

For a complete example of how we can setup a project and use the various available authentication methods, please refer to our quickstart example.

Authenticating a user is easy using the useAuthenticate() hook from the @account-kit/react-native package. Before we can use that, you’ll need to configure your application to associate it with a domain you control.

Step 1: Set an rpId in createConfig

The rpId (“relaying party ID”) specifies the domain on which passkeys are allowed to function. While passkeys on web applications are automatically associated with the website’s domain, mobile applications must be registered with a domain to prove that they are associated.

In your call to createConfig, pass an rpId parameter set to a domain you control. Note that the scheme is always assumed to be “https://” and should be omitted.

1const config = createConfig({
2 // ... other config
3 rpId: "your-domain.com",
4});

Step 2: Host a Site Association JSON

While passkeys on web applications are automatically associated with the website’s domain, mobile applications must be registered on that domain to prove that they are associated. To do so, you will need to host a JSON file referencing your app on your domain. The details of doing so differ on iOS and Android.

iOS configuration

More information in Apple docs

On your webserver, set up the route

GET https://<yourdomain>/.well-known/apple-app-site-association

This route should serve a static JSON object containing your team id and identifier. You should replace <team-identifier> and <bundle-id> in the below snippet, so it might appear as e.g. H123456789.com.yourapp.passkeyExample.

1{
2 "applinks": {},
3 "webcredentials": {
4 "apps": ["<team-identifier>.<bundle-id>"]
5 },
6 "appclips": {}
7}

Next, in XCode under “Signing & Capabilities”, add a new capability of type “Associated Domains”. Now add the following, replacing <yourdomain> with the domain on which you hosted the JSON (e.g. your-domain.com):

webcredentials:<yourdomain>

Android configuration

More information in Android docs

On your webserver, set up the route

GET https://<yourdomain>/.well-known/assetlinks.json

This route should serve a static JSON object containing the following information:

1[
2 {
3 "relation": ["delegate_permission/common.get_login_creds"],
4 "target": {
5 "namespace": "android_app",
6 "package_name": "<your-package-name>",
7 "sha256_cert_fingerprints": ["<sha-hex-value>"]
8 }
9 }
10]

You should replace <your-package-name> with the package name, e.g. com.yourapp.passkeyExample, and "<sha-hex-value>" with the SHA256 fingerprints of your app’s [signing certificate], e.g. "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:12:75:91:03:3B:9C".

Step 3: Add a Passkey

You have the option of creating an account with passkey and email or with passkey alone.

If you create a passkey without an email associated with the user, you risk your users losing access to their wallets if they lose their device.

Recommended security practice: Proxy authentication requests to your backend server to enforce additional security measures:

  • When a user attempts to sign up with both passkey and email, you can first require email verification before allowing the passkey to be created
  • Alternatively, you can restrict initial signup to email-based methods only (which inherently verify email ownership), then allow users to add passkeys after their account is established
  • This approach gives you greater control over the authentication flow and helps prevent account recovery issues

By implementing server-side verification, you ensure that passkeys are only created for verified identities, reducing the risk of permanent access loss.

import { 
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
} from "@account-kit/react-native";
import React, {
function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)

Returns a stateful value, and a function to update it.

useState
} from "react";
import {
type Alert = AlertStatic const Alert: AlertStatic
Alert
,
class View
View
,
class Text
Text
,
class TextInput
TextInput
,
class Button
Button
,
const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>
Pressable
} from "react-native";
function
function CreatePasskeyAndEmail(): JSX.Element
CreatePasskeyAndEmail
() {
const {
const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>
authenticate
} =
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
();
const [
const email: string
email
,
const setEmail: React.Dispatch<React.SetStateAction<string>>
setEmail
] =
useState<string>(initialState: string | (() => string)): [string, React.Dispatch<React.SetStateAction<string>>] (+1 overload)

Returns a stateful value, and a function to update it.

useState
("");
const
const handleCreatePasskeyAndEmail: () => void
handleCreatePasskeyAndEmail
= () => {
// Important: Validate the email before proceeding if (!
const isValidEmail: (email: string) => boolean
isValidEmail
(
const email: string
email
)) {
// Handle validation error return; } try {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => void
authenticate
({
type: "passkey"
type
: "passkey",
email: string
email
,
}); // Prompt the user to create a passkey, and create an account once they do. } catch (
function (local var) e: unknown
e
) {
const Alert: AlertStatic
Alert
.
AlertStatic.alert: (title: string, message?: string, buttons?: AlertButton[], options?: AlertOptions) => void
alert
("Error creating passkey. Check logs for more details.");
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream. * A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:


const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```

Example using the `Console` class:

```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);

myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console
.
Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)
log
("Error creating passkey: ",
function (local var) e: unknown
e
);
} }; return ( <
class View
View
>
<
class Text
Text
>Enter Your Email to Create Account</
class Text
Text
>
<
class View
View
>
<
class TextInput
TextInput
value?: string | undefined

The value to show for the text input. TextInput is a controlled component, which means the native value will be forced to match this value prop if provided. For most uses this works great, but in some cases this may cause flickering one common cause is preventing edits by keeping value the same. In addition to simply setting the same value, either set editable=false, or set/update maxLength to prevent unwanted edits without flicker.

value
={
const email: string
email
}
onChangeText?: ((text: string) => void) | undefined

Callback that is called when the text input's text changes. Changed text is passed as an argument to the callback handler.

onChangeText
={(
val: string
val
) =>
const setEmail: (value: React.SetStateAction<string>) => void
setEmail
(
val: string
val
.
String.toLowerCase(): string

Converts all the alphabetic characters in a string to lowercase.

toLowerCase
())}
placeholder?: string | undefined

The string that will be rendered before text input has been entered

placeholder
="[email protected]"
/> <
const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>
Pressable
PressableProps.onPress?: ((event: GestureResponderEvent) => void) | null | undefined

Called when a single tap gesture is detected.

onPress
={
const handleCreatePasskeyAndEmail: () => void
handleCreatePasskeyAndEmail
}>
{({
pressed: boolean
pressed
}) => (
<
class View
View
style?: StyleProp<ViewStyle>
style
={[
{
ViewStyle.opacity?: AnimatableNumericValue | undefined
opacity
:
pressed: boolean
pressed
? 0.5 : 1,
TransformsStyle.transform?: string | readonly (({ perspective: AnimatableNumericValue; } & { rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; scaleX?: undefined; ... 5 more ...; matrix?: undefined; }) | ... 11 more ... | ({ ...; } & { ...; }))[] | undefined
transform
: [
{
scale: AnimatableNumericValue
scale
:
pressed: boolean
pressed
? 0.98 : 1,
}, ], }, ]} > <
class Text
Text
>Sign In</
class Text
Text
>
</
class View
View
>
)} </
const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>
Pressable
>
</
class View
View
>
</
class View
View
>
); } // Simple email validation function const
const isValidEmail: (email: string) => boolean
isValidEmail
= (
email: string
email
: string) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.
RegExp.test(string: string): boolean

Returns a Boolean value that indicates whether or not a pattern exists in a searched string.

test
(
email: string
email
);
};

It’s important that you validate the email before creating an account for the user. This is to prevent users from losing access to their wallets if they lose their device.

Option 2: Creating a New Account

To create an account with a passkey, use the authenticate() function, with the type set to "passkey" and createNew set to true.

import { 
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
} from "@account-kit/react-native";
import React, {
function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)

Returns a stateful value, and a function to update it.

useState
} from "react";
import {
type Alert = AlertStatic const Alert: AlertStatic
Alert
,
class View
View
,
class Text
Text
,
class TextInput
TextInput
,
class Button
Button
,
const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>
Pressable
} from "react-native";
function
function CreatePasskey(): JSX.Element
CreatePasskey
() {
const {
const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>
authenticate
} =
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
();
const
const handleCreatePasskey: () => void
handleCreatePasskey
= () => {
try {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => void
authenticate
({
type: "passkey"
type
: "passkey",
createNew: true
createNew
: true,
// This will be the name of the saved passkey on the user's device.
username: string
username
: "Your App user",
}); } catch (
function (local var) e: unknown
e
) {
const Alert: AlertStatic
Alert
.
AlertStatic.alert: (title: string, message?: string, buttons?: AlertButton[], options?: AlertOptions) => void
alert
("Error creating passkey. Check logs for more details.");
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream. * A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:


const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```

Example using the `Console` class:

```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);

myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console
.
Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)
log
("Error creating passkey: ",
function (local var) e: unknown
e
);
} }; return ( <
class View
View
>
<
class Text
Text
>Create an account with a passkey</
class Text
Text
>
<
class View
View
>
<
const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>
Pressable
PressableProps.onPress?: ((event: GestureResponderEvent) => void) | null | undefined

Called when a single tap gesture is detected.

onPress
={
const handleCreatePasskey: () => void
handleCreatePasskey
}>
{({
pressed: boolean
pressed
}) => (
<
class View
View
style?: StyleProp<ViewStyle>
style
={[
{
ViewStyle.opacity?: AnimatableNumericValue | undefined
opacity
:
pressed: boolean
pressed
? 0.5 : 1,
TransformsStyle.transform?: string | readonly (({ perspective: AnimatableNumericValue; } & { rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; scaleX?: undefined; ... 5 more ...; matrix?: undefined; }) | ... 11 more ... | ({ ...; } & { ...; }))[] | undefined
transform
: [
{
scale: AnimatableNumericValue
scale
:
pressed: boolean
pressed
? 0.98 : 1,
}, ], }, ]} > <
class Text
Text
>Create account</
class Text
Text
>
</
class View
View
>
)} </
const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>
Pressable
>
</
class View
View
>
</
class View
View
>
); }

Step 4: Sign In with a Passkey

To sign in with an existing passkey, use the authenticate() function, with the type set to "passkey".

import { 
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
} from "@account-kit/react-native";
import React, {
function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)

Returns a stateful value, and a function to update it.

useState
} from "react";
import {
type Alert = AlertStatic const Alert: AlertStatic
Alert
,
class View
View
,
class Text
Text
,
class TextInput
TextInput
,
class Button
Button
,
const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>
Pressable
} from "react-native";
function
function SignInWithPasskey(): JSX.Element
SignInWithPasskey
() {
const {
const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>
authenticate
} =
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
();
const
const handleSignIn: () => void
handleSignIn
= () => {
try {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => void
authenticate
({
type: "passkey"
type
: "passkey",
createNew: false
createNew
: false,
}); } catch (
function (local var) e: unknown
e
) {
const Alert: AlertStatic
Alert
.
AlertStatic.alert: (title: string, message?: string, buttons?: AlertButton[], options?: AlertOptions) => void
alert
(
"Error signing in with passkey. Check logs for more details.", );
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream. * A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:


const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```

Example using the `Console` class:

```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);

myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console
.
Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)
log
("Error signing in with passkey: ",
function (local var) e: unknown
e
);
} }; return ( <
class View
View
>
<
class Text
Text
>Sign in with passkey</
class Text
Text
>
<
class View
View
>
<
const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>
Pressable
PressableProps.onPress?: ((event: GestureResponderEvent) => void) | null | undefined

Called when a single tap gesture is detected.

onPress
={
const handleSignIn: () => void
handleSignIn
}>
{({
pressed: boolean
pressed
}) => (
<
class View
View
style?: StyleProp<ViewStyle>
style
={[
{
ViewStyle.opacity?: AnimatableNumericValue | undefined
opacity
:
pressed: boolean
pressed
? 0.5 : 1,
TransformsStyle.transform?: string | readonly (({ perspective: AnimatableNumericValue; } & { rotate?: undefined; rotateX?: undefined; rotateY?: undefined; rotateZ?: undefined; scale?: undefined; scaleX?: undefined; ... 5 more ...; matrix?: undefined; }) | ... 11 more ... | ({ ...; } & { ...; }))[] | undefined
transform
: [
{
scale: AnimatableNumericValue
scale
:
pressed: boolean
pressed
? 0.98 : 1,
}, ], }, ]} > <
class Text
Text
>Sign In</
class Text
Text
>
</
class View
View
>
)} </
const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>
Pressable
>
</
class View
View
>
</
class View
View
>
); }