Turnkey’s Embedded Wallets enable you to integrate secure, custom wallet experiences directly into your product. With features like advanced security, seamless authentication, and flexible UX options, you can focus on building great products while we handle the complexities of private key management.
Create a new Next.js app via npx create-next-app@latest. Or install into an existing project.
Copy
Ask AI
npm install @turnkey/sdk-react
Technical Requirements
Next.js App Router Required
@turnkey/sdk-react requires the Next.js App Router architecture as it leverages React Server Components and Server Actions to handle authentication on your behalf. The SDK automatically manages the “use client” and “use server” directives. The Pages Router is not supported.
React 19 Users
If you’re using Next.js 15 with React 19 you may encounter an installation error with @turnkey/sdk-react. Consider:
The following environment variables are necessary to use the Turnkey SDK.
.env
Copy
Ask AI
NEXT_PUBLIC_ORGANIZATION_ID=<your turnkey org id>TURNKEY_API_PUBLIC_KEY=<your api public key>TURNKEY_API_PRIVATE_KEY=<your api private key>NEXT_PUBLIC_BASE_URL=https://api.turnkey.com
Note: These environment variable names must be used exactly as shown.
The SDK depends on them internally for server actions and authentication.
2
Configure
Fill in with your Organization ID and API Base URL.
The auth component contains the UI and logic to handle the authentication flow.
1
Configure
For simplicity, this app will only support email authentication. We have other guides on additional authentication methods. Additionally, you can customize the order in which the auth methods are displayed.
src/app/page.tsx
Copy
Ask AI
"use client";export default function Home() { // The auth methods to display in the UI const config = { authConfig: { emailEnabled: true, // Set the rest to false to disable them passkeyEnabled: false, phoneEnabled: false, appleEnabled: false, facebookEnabled: false, googleEnabled: false, }, // The order of the auth methods to display in the UI configOrder: ["email" /* "passkey", "phone", "socials" */], }; return <div></div>;}
Define two functions to handle the “success” and “error” states. Initially, the onError function will set an errorMessage state variable which will be used to display an error message to the user. The onAuthSuccess function will route the user to the dashboard after successful authentication.
A new sub-organization and wallet is created for each new user during the authentication flow.
src/app/page.tsx
Copy
Ask AI
"use client";import { useState } from "react";import { Auth } from "@turnkey/sdk-react";export default function Home() { const [errorMessage, setErrorMessage] = useState(""); const router = useRouter(); const onAuthSuccess = async () => { // We'll add the dashboard route in the next step router.push("/dashboard"); }; const onError = (errorMessage: string) => { setErrorMessage(errorMessage); }; // Add the handlers to the config object const config = { // ... onAuthSuccess: onAuthSuccess, onError: onError, }; return ( <div> <Auth {...config} /> </div> );}
4
Dashboard: User Session
Add a dashboard route to the app where the user will be able to view their account and sign messages.
src/app/dashboard/page.tsx
Copy
Ask AI
export default function Dashboard() { return <div>Dashboard</div>;}
Since the app is wrapped with the TurnkeyProvider component, the useTurnkey hook is available to all child components. Calling turnkey.getCurrentUser() will return the current user’s session information from local storage.
Add a state variable to store the user:
src/app/dashboard/page.tsx
Copy
Ask AI
import { useState, useEffect } from "react";import { useTurnkey } from "@turnkey/sdk-react";export default function Dashboard() { const { turnkey } = useTurnkey(); const [user, setUser] = useState<User | null>(null); useEffect(() => { if (turnkey) { const user = turnkey.getCurrentUser(); setUser(user); } }, [turnkey]); return <div>Dashboard</div>;}
export type Session = { sessionType: SessionType; userId: string; organizationId: string; expiry: number; // Unix timestamp representing the expiry of the session set by the server token: string; // publicKey used to verify stamped requests (lives in IndexedDB); can temporarily be a read bearer token};
Turnkey supports signing arbitrary messages with the signRawPayload method.
The signRawPayload method requires these parameters:
payload: The raw unsigned payload to sign
signWith: The signing address (wallet account, private key address, or private key ID)
encoding: The message encoding format
hashFunction: The selected hash algorithm
1
The Payload
For simplicity, a human readable string, message, will be the payload to sign. Add a state variable to store the message and an input field to allow the user to enter the message:
src/app/dashboard/page.tsx
Copy
Ask AI
import { useState, useEffect } from "react";export default function Dashboard() {//...const [message, setMessage] = useState("");//...return ( <div> <input type="text" value={message} onChange={(e) => setMessage(e.target.value)} placeholder="Enter message to sign" /> </div>);}
2
The Signer
Signing messages requires a signer e.g. a Turnkey wallet address to sign with and a payload or message to sign. A new wallet is created for each user during the authentication flow.
Create a function called getSignWith, to get the user’s wallet account address which will be used to sign the message.
Use the getSession method from the useTurnkey hook to get the user’s read-write session:
src/app/dashboard/page.tsx
Copy
Ask AI
import { useState, useEffect } from "react";import { useTurnkey, indexedDbClient } from "@turnkey/sdk-react";export default function Dashboard() { const { turnkey, indexedDbClient } = useTurnkey(); const [user, setUser] = useState<User | null>(null); const getSignWith = async () => { const session = await turnkey?.getSession(); if (session) { const client = } else { // log out and clear session } const client = indexedDbClient; // The user's sub-organization id const organizationId = user?.organization.organizationId; // Get the user's wallets const wallets = await client?.getWallets({ organizationId, }); // Get the first wallet of the user const walletId = wallets?.wallets[0].walletId ?? ""; // Use the `walletId` to get the accounts associated with the wallet const accounts = await client?.getWalletAccounts({ organizationId, walletId, }); const signWith = accounts?.accounts[0].address ?? ""; return signWith; }; useEffect(/* ... */*/); return (/* <div>...</div> */*/);}
3
The Signing Function
Create a function called signMessage. This function will:
Get the user’s wallet account for signing the message
Compute the keccak256 hash of the message
Call the signRawPayload method
Note: To compute the keccak256 hash of the message, this example uses the hashMessage function from viem. However, any other hashing library can be used.
Copy
Ask AI
const signMessage = async () => { const payload = await hashMessage(message); const signWith = await getSignWith(); const signature = await client?.signRawPayload({ payload, signWith, // The message encoding format encoding: "PAYLOAD_ENCODING_TEXT_UTF8", // The hash function used to hash the message hashFunction: "HASH_FUNCTION_KECCAK256", });};
4
Display
Add a button to the UI to trigger the signMessage function.
src/app/dashboard/page.tsx
Copy
Ask AI
import { useState, useEffect } from "react";import { useTurnkey } from "@turnkey/sdk-react";import { hashMessage } from "viem";export default function Dashboard() { //... const [message, setMessage] = useState(""); const signMessage = async () => { const payload = await hashMessage(message); const signWith = await getSignWith(); const signature = await client?.signRawPayload({ payload, signWith, // The message encoding format encoding: "PAYLOAD_ENCODING_TEXT_UTF8", // The hash function used to hash the message hashFunction: "HASH_FUNCTION_KECCAK256", }); }; return ( <div> <h2>Sign Message</h2> <input type="text" value={message} onChange={(e) => setMessage(e.target.value)} placeholder="Enter message to sign" /> <button onClick={signMessage}>Sign</button> </div> );}