Begin by initializing the Turnkey SDK with your organization ID and the Turnkey API’s base URL on the client-side.
Wrap the root layout of your application with the TurnkeyProvider providing the required configuration options. This allows you to use the Turnkey client throughout your app via the useTurnkey() hook.
The NEXT_PUBLIC_ORGANIZATION_ID should be set to the parent organization ID which can be found in the Turnkey Dashboard.
The NEXT_PUBLIC_TURNKEY_RP_ID should be set to your application’s desired relying party ID; this is typically your domain, or localhost if developing locally. See this page for more details.
Wrap the root layout of your application with the TurnkeyProvider providing the required configuration options. This allows you to use the Turnkey client throughout your app via the useTurnkey() hook.
The NEXT_PUBLIC_ORGANIZATION_ID should be set to the parent organization ID which can be found in the Turnkey Dashboard.
The NEXT_PUBLIC_TURNKEY_RP_ID should be set to your application’s desired relying party ID; this is typically your domain, or localhost if developing locally. See this page for more details.
src/turnkey.ts
Copy
Ask AI
import { Turnkey } from "@turnkey/sdk-browser";// Initialize the Turnkey SDK with your organization ID and API base URLconst turnkeyBrowser = new Turnkey({ rpId: process.env.TURNKEY_RP_ID, apiBaseUrl: "https://api.turnkey.com", defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID,});
The TURNKEY_ORGANIZATION_ID should be set to the parent organization ID which can be found in the Turnkey Dashboard.
The TURNKEY_RP_ID should be set to your application’s desired relying party ID; this is typically your domain, or localhost if developing locally. See this page for more details.
Initialize the Turnkey SDK on the server-side using the @turnkey/sdk-server package. This allows you to use the parent organization’s public/private API key pair to initialize the email recovery process securely.
For Next.js, add the "use server" directive at the top of the file where you’re initializing the Turnkey server client. This will ensure that the function is executed on the server-side and will have access to the server-side environment variables e.g. your parent organization’s public/private API key pair. For more information on Next.js server actions, see the Next.js documentation on Server Actions and Mutations.
app/actions.ts
Copy
Ask AI
"use server";import { Turnkey } from "@turnkey/sdk-server";// Initialize the Turnkey Server Client on the server-sideconst turnkeyServer = new Turnkey({ apiBaseUrl: "https://api.turnkey.com", apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY, apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY, defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID,}).apiClient();
For Next.js, add the "use server" directive at the top of the file where you’re initializing the Turnkey server client. This will ensure that the function is executed on the server-side and will have access to the server-side environment variables e.g. your parent organization’s public/private API key pair. For more information on Next.js server actions, see the Next.js documentation on Server Actions and Mutations.
app/actions.ts
Copy
Ask AI
"use server";import { Turnkey } from "@turnkey/sdk-server";// Initialize the Turnkey Server Client on the server-sideconst turnkeyServer = new Turnkey({ apiBaseUrl: "https://api.turnkey.com", apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY, apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY, defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID,}).apiClient();
src/turnkey.ts
Copy
Ask AI
import { Turnkey } from "@turnkey/sdk-server";// Initialize the Turnkey Server Client on the server-sideconst turnkeyServer = new Turnkey({ apiBaseUrl: "https://api.turnkey.com", apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY, apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY, defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID,}).apiClient();
Next, we’ll initialize the iframeClient which will create a secure iframe within your application. The iframeClient must be initialized before beginning the user recovery process, as we’ll need the iframe’s public key as a parameter for the initEmailRecovery method.
We add the "use client" directive to the Recovery component to as react hooks can only be used client-side.
app/recovery.tsx
Copy
Ask AI
"use client";import { useTurnkey } from "@turnkey/sdk-react";export default function Recovery() { const { authIframeClient } = useTurnkey(); return <div>{/* ... rest of the code */}</div>;}
We add the "use client" directive to the Recovery component to as react hooks can only be used client-side.
app/recovery.tsx
Copy
Ask AI
"use client";import { useTurnkey } from "@turnkey/sdk-react";export default function Recovery() { const { authIframeClient } = useTurnkey(); return <div>{/* ... rest of the code */}</div>;}
Next we’ll create a new function called initEmailRecovery that will be used to initialize the email recovery process on the server-side. This method will be called from the client-side with the user’s email and the target public key from the iframe client. Calling the initEmailRecovery method will trigger an email sent to the user containing a credential bundle which will be used to authenticate the authIframeClient in the next step.
We export the initEmailRecovery server action to be called from the client-side.
At this stage, we initialize the email recovery process using the server-side function we created in the previous step. The user will need to paste the credential bundle they receive in their email into your app, which is then used to authenticate the authIframeClient via the injectCredentialBundle method.
1
Import the server action
app/recovery.tsx
Copy
Ask AI
import { initEmailRecovery } from "./actions";
2
Add an input field for the user's email
app/recovery.tsx
Copy
Ask AI
// ...export default function Recovery() { // ... // Create a state variable for the user's email const [email, setEmail] = useState(""); return ( <div> <input value={email} onChange={(e) => setEmail(e.target.value)} type="text" /> </div> );}
3
Create a function to initiate the recovery process
app/recovery.tsx
Copy
Ask AI
//...export default function Recovery() { // ... // We'll use this later to conditionally render the input for the credential bundle const [initRecoveryResponse, setInitRecoveryResponse] = useState(null); const initRecovery = async (email: string) => { // Call the initEmailRecovery server action const response = await initEmailRecovery({ email, targetPublicKey: authIframeClient?.iframePublicKey, }); if (response) { setInitRecoveryResponse(response); } }; return ( <div> {/* <input ... /> */} <button onClick={() => initRecovery(email)}>Init Recovery</button> </div> );}
4
Add an input for the credential bundle
app/recovery.tsx
Copy
Ask AI
//...export default function Recovery() { // ... const [initRecoveryResponse, setInitRecoveryResponse] = useState(null); const [credentialBundle, setCredentialBundle] = useState(""); return ( <div> {/* If we have initiated the recovery process we'll render an input for the user to paste their credential bundle they received in their email */} {initRecoveryResponse ? ( <input value={credentialBundle} onChange={(e) => setCredentialBundle(e.target.value)} type="text" /> ) : ( <input value={email} onChange={(e) => setEmail(e.target.value)} type="text" /> )} <button onClick={() => initRecovery(email)}>Init Recovery</button> </div> );}
Copy
Ask AI
"use client";import { useState } from "react";import { useTurnkey } from "@turnkey/sdk-react";// Import the initEmailRecovery server actionimport { initEmailRecovery } from "./actions";export default function Recovery() { const { authIframeClient } = useTurnkey(); // Create a state variable for the user's email const [email, setEmail] = useState(""); const [initRecoveryResponse, setInitRecoveryResponse] = useState(null); const [credentialBundle, setCredentialBundle] = useState(""); const initRecovery = async (email: string) => { // Call the initEmailRecovery server action const response = await initEmailRecovery({ email, targetPublicKey: authIframeClient?.iframePublicKey, }); if (response) { setInitRecoveryResponse(response); } }; return ( <div> {/* If we have initiated the recovery process we'll render an input for the user to paste their credential bundle they received in their email */} {initRecoveryResponse ? ( <input value={credentialBundle} onChange={(e) => setCredentialBundle(e.target.value)} type="text" /> ) : ( <input value={email} onChange={(e) => setEmail(e.target.value)} type="text" /> )} <button onClick={() => initRecovery(email)}>Init Recovery</button> </div> );}
1
Import the server action
app/recovery.tsx
Copy
Ask AI
import { initEmailRecovery } from "./actions";
2
Add an input field for the user's email
app/recovery.tsx
Copy
Ask AI
// ...export default function Recovery() { // ... // Create a state variable for the user's email const [email, setEmail] = useState(""); return ( <div> <input value={email} onChange={(e) => setEmail(e.target.value)} type="text" /> </div> );}
3
Create a function to initiate the recovery process
app/recovery.tsx
Copy
Ask AI
//...export default function Recovery() { // ... // We'll use this later to conditionally render the input for the credential bundle const [initRecoveryResponse, setInitRecoveryResponse] = useState(null); const initRecovery = async (email: string) => { // Call the initEmailRecovery server action const response = await initEmailRecovery({ email, targetPublicKey: authIframeClient?.iframePublicKey, }); if (response) { setInitRecoveryResponse(response); } }; return ( <div> {/* <input ... /> */} <button onClick={() => initRecovery(email)}>Init Recovery</button> </div> );}
4
Add an input for the credential bundle
app/recovery.tsx
Copy
Ask AI
//...export default function Recovery() { // ... const [initRecoveryResponse, setInitRecoveryResponse] = useState(null); const [credentialBundle, setCredentialBundle] = useState(""); return ( <div> {/* If we have initiated the recovery process we'll render an input for the user to paste their credential bundle they received in their email */} {initRecoveryResponse ? ( <input value={credentialBundle} onChange={(e) => setCredentialBundle(e.target.value)} type="text" /> ) : ( <input value={email} onChange={(e) => setEmail(e.target.value)} type="text" /> )} <button onClick={() => initRecovery(email)}>Init Recovery</button> </div> );}
Copy
Ask AI
"use client";import { useState } from "react";import { useTurnkey } from "@turnkey/sdk-react";// Import the initEmailRecovery server actionimport { initEmailRecovery } from "./actions";export default function Recovery() { const { authIframeClient } = useTurnkey(); // Create a state variable for the user's email const [email, setEmail] = useState(""); const [initRecoveryResponse, setInitRecoveryResponse] = useState(null); const [credentialBundle, setCredentialBundle] = useState(""); const initRecovery = async (email: string) => { // Call the initEmailRecovery server action const response = await initEmailRecovery({ email, targetPublicKey: authIframeClient?.iframePublicKey, }); if (response) { setInitRecoveryResponse(response); } }; return ( <div> {/* If we have initiated the recovery process we'll render an input for the user to paste their credential bundle they received in their email */} {initRecoveryResponse ? ( <input value={credentialBundle} onChange={(e) => setCredentialBundle(e.target.value)} type="text" /> ) : ( <input value={email} onChange={(e) => setEmail(e.target.value)} type="text" /> )} <button onClick={() => initRecovery(email)}>Init Recovery</button> </div> );}
src/turnkey.ts
Copy
Ask AI
import { initEmailRecovery } from "./turnkey-server";// ... rest of the codeconst initRecoveryResponse = await initEmailRecovery({ email, targetPublicKey: authIframeClient?.iframePublicKey,});// Inject the recovery bundle into the iframe client// The recovery bundle is the credential bundle that the user will receive in their email// The application will need to provide a way for the user to input this recovery bundle// by pasting it into the UIawait authIframeClient.injectCredentialBundle(credentialBundle);
Next, we’ll create a new passkey for the user and associate it with the email that was used in the recovery process. Assuming that the user has successfully received and entered their credential bundle, we generate a passkey to be used authenticate Turnkey requests.
1
Add a function to complete the recovery process
We’ll add a new function called completeRecovery that will create a new passkey for the user which will be used in the final recovery step.
app/recovery.tsx
Copy
Ask AI
//...export default function Recovery() { //... // We'll use //...export default function Recovery() { //... // We'll use the useTurnkey hook to get the turnkey instance const { authIframeClient, turnkey } = useTurnkey(); const completeRecovery = async () => { const passkeyClient = await turnkey.passkeyClient(); const passkeyResponse = await passkeyClient?.createUserPasskey({ publicKey: { user: { name: email, displayName: email, }, }, }); }; return <div>{/* ... */}</div>;}
2
Add a button to call the completeRecovery function
app/recovery.tsx
Copy
Ask AI
//...export default function Recovery() { //... const completeRecovery = async () => {/* ... */*/}; return ( <div> {/* ... */} {/* If we have the credential bundle, we'll render a button to complete the recovery process */} {credentialBundle ? ( <button onClick={() => completeRecovery(credentialBundle)}> Complete Recovery </button> ) : ( <button onClick={() => initRecovery(email)}>Init Recovery</button> )} </div> );}
1
Add a function to complete the recovery process
We’ll add a new function called completeRecovery that will create a new passkey for the user which will be used in the final recovery step.
app/recovery.tsx
Copy
Ask AI
//...export default function Recovery() { //... // We'll use //...export default function Recovery() { //... // We'll use the useTurnkey hook to get the turnkey instance const { authIframeClient, turnkey } = useTurnkey(); const completeRecovery = async () => { const passkeyClient = await turnkey.passkeyClient(); const passkeyResponse = await passkeyClient?.createUserPasskey({ publicKey: { user: { name: email, displayName: email, }, }, }); }; return <div>{/* ... */}</div>;}
2
Add a button to call the completeRecovery function
app/recovery.tsx
Copy
Ask AI
//...export default function Recovery() { //... const completeRecovery = async () => {/* ... */*/}; return ( <div> {/* ... */} {/* If we have the credential bundle, we'll render a button to complete the recovery process */} {credentialBundle ? ( <button onClick={() => completeRecovery(credentialBundle)}> Complete Recovery </button> ) : ( <button onClick={() => initRecovery(email)}>Init Recovery</button> )} </div> );}
Finally, we complete the email recovery process by passing the encodedChallenge and attestation from the passkey we previously created to the recoverUser method. This method will complete the email recovery process and if successful, will return a response containing the authenticator ID of the new passkey authenticator.
app/recovery.tsx
Copy
Ask AI
//...export default function Recovery() { // We'll use the useTurnkey hook to get the turnkey instance const { authIframeClient, turnkey } = useTurnkey(); const [initRecoveryResponse, setInitRecoveryResponse] = useState(null); const completeRecovery = async () => { const passkeyClient = await turnkey.passkeyClient(); const passkeyResponse = await passkeyClient?.createUserPasskey({ publicKey: { user: { name: email, displayName: email, }, }, }); // If we have the encodedChallenge and attestation, we can complete the recovery process if (passkeyResponse?.encodedChallenge && passkeyResponse?.attestation) { const response = await authIframeClient!.recoverUser({ organizationId: initRecoveryResponse?.activity.organizationId, userId: initRecoveryResponse.userId, authenticator: { // This should be set by the user to name their authenticator authenticatorName: "User Passkey", challenge: passkeyResponse.encodedChallenge, attestation: passkeyResponse.attestation, }, }); if (response) { console.log("User recovered successfully"); } } }; return ( <div> {/* ... */} {/* If we have the credential bundle, we'll render a button to complete the recovery process */} {credentialBundle ? ( <button onClick={() => completeRecovery(credentialBundle)}> Complete Recovery </button> ) : ( <button onClick={() => initRecovery(email)}>Init Recovery</button> )} </div> );}
app/recovery.tsx
Copy
Ask AI
"use client";import { useState } from "react";import { useTurnkey } from "@turnkey/sdk-react";// Import the initEmailRecovery server actionimport { initEmailRecovery } from "./actions";export default function Recovery() { const { authIframeClient, turnkey } = useTurnkey(); // Create a state variable for the user's email const [email, setEmail] = useState(""); const [initRecoveryResponse, setInitRecoveryResponse] = useState(null); const [credentialBundle, setCredentialBundle] = useState(""); const initRecovery = async (email: string) => { // Call the initEmailRecovery server action const response = await initEmailRecovery({ email, targetPublicKey: authIframeClient?.iframePublicKey, }); if (response) { setInitRecoveryResponse(response); } }; const completeRecovery = async () => { const passkeyClient = await turnkey.passkeyClient(); const passkeyResponse = await passkeyClient?.createUserPasskey({ publicKey: { user: { name: email, displayName: email, }, }, }); // If we have the encodedChallenge and attestation, we can complete the recovery process if (passkeyResponse?.encodedChallenge && passkeyResponse?.attestation) { const response = await authIframeClient!.recoverUser({ organizationId: initRecoveryResponse?.activity.organizationId, userId: initRecoveryResponse.userId, authenticator: { // This should be set by the user to name their authenticator authenticatorName: "User Passkey", challenge: passkeyResponse.encodedChallenge, attestation: passkeyResponse.attestation, }, }); if (response) { console.log("User recovered successfully"); } } }; return ( <div> {initRecoveryResponse ? ( <input value={credentialBundle} onChange={(e) => setCredentialBundle(e.target.value)} type="text" /> ) : ( <input value={email} onChange={(e) => setEmail(e.target.value)} type="text" /> )} {credentialBundle ? ( <button onClick={() => completeRecovery(credentialBundle)}> Complete Recovery </button> ) : ( <button onClick={() => initRecovery(email)}>Init Recovery</button> )} </div> );}
app/recovery.tsx
Copy
Ask AI
//...export default function Recovery() { // We'll use the useTurnkey hook to get the turnkey instance const { authIframeClient, turnkey } = useTurnkey(); const [initRecoveryResponse, setInitRecoveryResponse] = useState(null); const completeRecovery = async () => { const passkeyClient = await turnkey.passkeyClient(); const passkeyResponse = await passkeyClient?.createUserPasskey({ publicKey: { user: { name: email, displayName: email, }, }, }); // If we have the encodedChallenge and attestation, we can complete the recovery process if (passkeyResponse?.encodedChallenge && passkeyResponse?.attestation) { const response = await authIframeClient!.recoverUser({ organizationId: initRecoveryResponse?.activity.organizationId, userId: initRecoveryResponse.userId, authenticator: { // This should be set by the user to name their authenticator authenticatorName: "User Passkey", challenge: passkeyResponse.encodedChallenge, attestation: passkeyResponse.attestation, }, }); if (response) { console.log("User recovered successfully"); } } }; return ( <div> {/* ... */} {/* If we have the credential bundle, we'll render a button to complete the recovery process */} {credentialBundle ? ( <button onClick={() => completeRecovery(credentialBundle)}> Complete Recovery </button> ) : ( <button onClick={() => initRecovery(email)}>Init Recovery</button> )} </div> );}
app/recovery.tsx
Copy
Ask AI
"use client";import { useState } from "react";import { useTurnkey } from "@turnkey/sdk-react";// Import the initEmailRecovery server actionimport { initEmailRecovery } from "./actions";export default function Recovery() { const { authIframeClient, turnkey } = useTurnkey(); // Create a state variable for the user's email const [email, setEmail] = useState(""); const [initRecoveryResponse, setInitRecoveryResponse] = useState(null); const [credentialBundle, setCredentialBundle] = useState(""); const initRecovery = async (email: string) => { // Call the initEmailRecovery server action const response = await initEmailRecovery({ email, targetPublicKey: authIframeClient?.iframePublicKey, }); if (response) { setInitRecoveryResponse(response); } }; const completeRecovery = async () => { const passkeyClient = await turnkey.passkeyClient(); const passkeyResponse = await passkeyClient?.createUserPasskey({ publicKey: { user: { name: email, displayName: email, }, }, }); // If we have the encodedChallenge and attestation, we can complete the recovery process if (passkeyResponse?.encodedChallenge && passkeyResponse?.attestation) { const response = await authIframeClient!.recoverUser({ organizationId: initRecoveryResponse?.activity.organizationId, userId: initRecoveryResponse.userId, authenticator: { // This should be set by the user to name their authenticator authenticatorName: "User Passkey", challenge: passkeyResponse.encodedChallenge, attestation: passkeyResponse.attestation, }, }); if (response) { console.log("User recovered successfully"); } } }; return ( <div> {initRecoveryResponse ? ( <input value={credentialBundle} onChange={(e) => setCredentialBundle(e.target.value)} type="text" /> ) : ( <input value={email} onChange={(e) => setEmail(e.target.value)} type="text" /> )} {credentialBundle ? ( <button onClick={() => completeRecovery(credentialBundle)}> Complete Recovery </button> ) : ( <button onClick={() => initRecovery(email)}>Init Recovery</button> )} </div> );}
src/turnkey.ts
Copy
Ask AI
const completeRecovery = async () => { const passkeyClient = await turnkey.passkeyClient(); const passkeyResponse = await passkeyClient?.createUserPasskey({ publicKey: { user: { name: email, displayName: email, }, }, }); // If we have the encodedChallenge and attestation, we can complete the recovery process if (passkeyResponse?.encodedChallenge && passkeyResponse?.attestation) { const response = await authIframeClient!.recoverUser({ organizationId: initRecoveryResponse?.activity.organizationId, userId: initRecoveryResponse.userId, authenticator: { // This should be set by the user to name their authenticator authenticatorName: "User Passkey", challenge: passkeyResponse.encodedChallenge, attestation: passkeyResponse.attestation, }, }); if (response) { console.log("User recovered successfully"); } }};
In this guide, we’ve walked through the process of recovering a user using their email using the Turnkey SDKs. By following these steps, you can implement email recovery in your application, providing users with a reliable way to regain access to their accounts or to onboard new users using only their email address.
To remind, this is a legacy flow, if you intend to implement a fresh recovery mechanism please use Email Auth, which supports all activities, including adding new authenticators.