Passkey Signup Authentication
Passkeys provide a secure, passwordless authentication method that can be used to create wallets for your users without going through email verification flows. You can implement passkey signup with or without an associated email address.
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.
You can implement Passkey Signup authentication in two ways:
- Pre-built UI Components - Quick implementation with minimal code
- Custom UI - Complete control over the user experience
Pre-built UI Components
Account Kit provides pre-built UI components that handle the entire Passkey Signup authentication flow with minimal code.
Step 1: Add Authentication Components to Your Page
Before configuring your authentication, first add one of the pre-built components to your application:
Using Modal Authentication
To add authentication in a modal popup:
import React from "react";
import { const useAuthModal: () => {
isOpen: boolean;
openAuthModal: () => void;
closeAuthModal: () => void;
}A hook that returns the open and close functions for the Auth Modal if uiConfig is enabled on the Account Provider
useAuthModal } from "@account-kit/react";
export default function function MyPage(): JSX.ElementMyPage() {
const { const openAuthModal: () => voidopenAuthModal } = function useAuthModal(): {
isOpen: boolean;
openAuthModal: () => void;
closeAuthModal: () => void;
}A hook that returns the open and close functions for the Auth Modal if uiConfig is enabled on the Account Provider
useAuthModal();
return <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={const openAuthModal: () => voidopenAuthModal}>Sign in</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>;
}
For more details on modal configuration, see the Modal Authentication documentation.
Or:
Using Embedded Authentication
To embed authentication directly in your page:
import React from "react";
import { const AuthCard: (props: AuthCardProps) => JSX.ElementReact component containing an Auth view with configured auth methods and options based on the config passed to the AlchemyAccountProvider
AuthCard } from "@account-kit/react";
export default function function MyLoginPage(): JSX.ElementMyLoginPage() {
return (
<React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div React.HTMLAttributes<HTMLDivElement>.className?: string | undefinedclassName="flex flex-row p-4 bg-white border border-gray-200 rounded-lg">
<const AuthCard: (props: AuthCardProps) => JSX.ElementReact component containing an Auth view with configured auth methods and options based on the config passed to the AlchemyAccountProvider
AuthCard />
</React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>
);
}
For more details on embedded authentication, see the Embedded Authentication documentation.
Step 2: Configure Passkey Signup in UI Components
After adding the components, configure the Passkey Signup authentication in your application config:
import { type AlchemyAccountsUIConfig = {
auth?: {
addPasskeyOnSignup?: boolean;
header?: React.ReactNode;
hideError?: boolean;
onAuthSuccess?: () => void;
sections: AuthType[][];
hideSignInText?: boolean;
};
illustrationStyle?: "outline" | "linear" | "filled" | "flat" | undefined;
modalBaseClassName?: string;
supportUrl?: string | undefined;
}AlchemyAccountsUIConfig, const createConfig: (props: CreateConfigProps, ui?: AlchemyAccountsUIConfig) => AlchemyAccountsConfigWithUIWraps the createConfig
that is exported from @aa-sdk/core
to allow passing an additional argument, the configuration object for the Auth Components UI (the modal and AuthCard).
createConfig } from "@account-kit/react";
import { const sepolia: Chainsepolia, function alchemy(config: AlchemyTransportConfig): AlchemyTransportCreates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.
alchemy } from "@account-kit/infra";
const const uiConfig: AlchemyAccountsUIConfiguiConfig: type AlchemyAccountsUIConfig = {
auth?: {
addPasskeyOnSignup?: boolean;
header?: React.ReactNode;
hideError?: boolean;
onAuthSuccess?: () => void;
sections: AuthType[][];
hideSignInText?: boolean;
};
illustrationStyle?: "outline" | "linear" | "filled" | "flat" | undefined;
modalBaseClassName?: string;
supportUrl?: string | undefined;
}AlchemyAccountsUIConfig = {
auth?: {
addPasskeyOnSignup?: boolean;
header?: React.ReactNode;
hideError?: boolean;
onAuthSuccess?: () => void;
sections: AuthType[][];
hideSignInText?: boolean;
} | undefinedauth: {
sections: AuthType[][]Each section can contain multiple auth types which will be grouped together and separated by an OR divider
sections: [
[
// Include passkey in a section
{ type: "passkey"type: "passkey" },
// You can combine with other authentication methods
{ type: "email"type: "email" },
],
],
// Enable automatic passkey creation after signup
addPasskeyOnSignup?: boolean | undefinedIf this is true, then auth components will prompt users to add a passkey after signing in for the first time
addPasskeyOnSignup: true,
},
};
export const const config: AlchemyAccountsConfigWithUIconfig = function createConfig(props: CreateConfigProps, ui?: AlchemyAccountsUIConfig): AlchemyAccountsConfigWithUIWraps the createConfig
that is exported from @aa-sdk/core
to allow passing an additional argument, the configuration object for the Auth Components UI (the modal and AuthCard).
createConfig(
{
transport: AlchemyTransporttransport: function alchemy(config: AlchemyTransportConfig): AlchemyTransportCreates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.
alchemy({ apiKey: stringapiKey: "your-api-key" }),
chain: Chainchain: const sepolia: Chainsepolia,
},
const uiConfig: AlchemyAccountsUIConfiguiConfig,
);
Passkey signup configuration accepts the following options:
type type PasskeyAuthType = {
type: "passkey";
}PasskeyAuthType = {
type: "passkey"type: "passkey";
};
You can find the full type definition in the Account Kit source code.
The key configuration option for passkey signup is addPasskeyOnSignup: true
, which will prompt users to create a passkey after they sign up with another method (like email).
For more details on UI component customization, see the UI Components documentation.
Custom UI
If you need complete control over the user experience, you can implement your own custom UI for Passkey Signup authentication using Account Kit hooks.
Option 1: Passkey Signup with Email (Recommended)
This approach associates an email with the passkey, allowing users to recover their account if they lose access to their device.
import { function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResultHook 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";
// Inside your component
const { const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>authenticate } = function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResultHook 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();
// When the user submits their email and wants to create a passkey
const const handlePasskeySignup: (email: string) => voidhandlePasskeySignup = (email: stringemail: string) => {
// Important: Validate the email before proceeding
if (!const isValidEmail: (email: string) => booleanisValidEmail(email: stringemail)) {
// Handle validation error
return;
}
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => voidauthenticate(
{
type: "passkey"type: "passkey",
email: stringemail,
},
{
MutateOptions<User, Error, AuthParams, unknown>.onSuccess?: ((data: User, variables: AuthParams, context: unknown) => void) | undefinedonSuccess: () => {
// Success - passkey created and user authenticated
},
MutateOptions<User, Error, AuthParams, unknown>.onError?: ((error: Error, variables: AuthParams, context: unknown) => void) | undefinedonError: (error: Errorerror) => {
// Handle error
},
},
);
};
// Simple email validation function
const const isValidEmail: (email: string) => booleanisValidEmail = (email: stringemail: string) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.RegExp.test(string: string): booleanReturns a Boolean value that indicates whether or not a pattern exists in a searched string.
test(email: stringemail);
};
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: Passkey Signup without Email
This approach creates a passkey without an associated email. Use this only if you have another recovery mechanism in place.
import { function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResultHook 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";
// Inside your component
const { const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>authenticate } = function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResultHook 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();
// When the user wants to create a passkey without email
const const handlePasskeyOnlySignup: (username: string) => voidhandlePasskeyOnlySignup = (username: stringusername: string) => {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => voidauthenticate(
{
type: "passkey"type: "passkey",
createNew: truecreateNew: true,
username: stringusername, // A unique identifier for the passkey
},
{
MutateOptions<User, Error, AuthParams, unknown>.onSuccess?: ((data: User, variables: AuthParams, context: unknown) => void) | undefinedonSuccess: () => {
// Success - passkey created and user authenticated
},
MutateOptions<User, Error, AuthParams, unknown>.onError?: ((error: Error, variables: AuthParams, context: unknown) => void) | undefinedonError: (error: Errorerror) => {
// Handle error
},
},
);
};
Step 3: Track Authentication Status
Use the useSignerStatus
hook to determine if the user is authenticated:
import { const useSignerStatus: (override?: AlchemyAccountContextProps) => UseSignerStatusResultHook to get the signer status, optionally using an override configuration, useful if you’re building your own login.
useSignerStatus } from "@account-kit/react";
// Inside your component
const { const isConnected: booleanisConnected } = function useSignerStatus(override?: AlchemyAccountContextProps): UseSignerStatusResultHook to get the signer status, optionally using an override configuration, useful if you’re building your own login.
useSignerStatus();
// You can use isConnected to conditionally render UI