Embedded Wallets Quickstart
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.
Prerequisites
This guide assumes you've completed the steps to create an account and organization as described in the Getting Started section.
Installation
Create a new Next.js app via npx create-next-app@latest
. Or install into an existing project.
- npm
- pnpm
- yarn
npm install @turnkey/sdk-react
pnpm add @turnkey/sdk-react
yarn add @turnkey/sdk-react
React 19 Users
If you're using Next.js 15 with React 19 you may encounter an installation error with @turnkey/sdk-react
. Consider:
- Downgrading React to
18.x.x
- Using
npm install --force
or--legacy-peer-deps
You may learn more about this here.
Setup
Environment
The following environment variables are necessary to use the Turnkey SDK.
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
Configure
Fill in with your Organization ID and API Base URL.
const config = {
apiBaseUrl: "https://api.turnkey.com",
defaultOrganizationId: process.env.NEXT_PUBLIC_TURNKEY_ORGANIZATION_ID,
};
Provider
Wrap your layout with the TurnkeyProvider
component.
import { TurnkeyProvider } from "@turnkey/sdk-react";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<TurnkeyProvider config={config}>{children}</TurnkeyProvider>
</body>
</html>
);
}
React 19 Users
@turnkey/sdk-react
is built with React 18. If you're using React 19 you'll find a type mismatch on the children type.
To fix this, you can use the @ts-ignore
directive to suppress the error.
<TurnkeyProvider config={config}>
{/* @ts-ignore */}
{children}
</TurnkeyProvider>
We're actively working towards React 19 compatibility.
Authenticate
The auth component contains the UI and logic to handle the authentication flow.
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.
"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>;
}
Auth Config Options
type AuthConfig = {
emailEnabled: boolean;
passkeyEnabled: boolean;
phoneEnabled: boolean;
appleEnabled: boolean;
googleEnabled: boolean;
facebookEnabled: boolean;
};
Import
Import the auth component into your app and pass in the config object.
"use client";
import { Auth } from "@turnkey/sdk-react";
export default function Home() {
const config = {
authConfig: {
emailEnabled: true,
passkeyEnabled: false,
phoneEnabled: false,
appleEnabled: false,
facebookEnabled: false,
googleEnabled: false,
},
configOrder: ["email"],
};
return (
<div>
<Auth {...config} />
</div>
);
}
Handlers
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.
"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>
);
}
Dashboard: User Session
Add a dashboard route to the app where the user will be able to view their account and sign messages.
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:
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>;
}
User Session
export interface User {
// Unique identifier for the user.
userId: string;
// Username of the user.
username: string;
organization: {
// Unique identifier for the organization.
organizationId: string;
// Name of the organization.
organizationName: string;
};
session:
| {
// read-only session .
read?: ReadOnlySession;
// read-write session details.
write?: ReadWriteSession;
// Authenticated client associated with the session.
authClient: AuthClient;
}
| undefined;
}
export interface ReadOnlySession {
// Read-only session token for `X-Session` header
token: string;
// Expiry time in seconds since Unix epoch.
expiry: number;
}
export interface ReadWriteSession {
// Credential bundle for iFrame client, generated by `createReadWriteSession` or `createApiKeys`.
credentialBundle: string;
// Expiry time in seconds since Unix epoch.
expiry: number;
}
Sign Message
Turnkey supports signing arbitrary messages with the signRawPayload
method.
The signRawPayload
method requires these parameters:
payload
: The raw unsigned payload to signsignWith
: The signing address (wallet account, private key address, or private key ID)encoding
: The message encoding formathashFunction
: The selected hash algorithm
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:
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>
);
}
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 getActiveClient
method from the useTurnkey
hook to get the client authenticated with the user's read-write session:
import { useState, useEffect } from "react";
import { useTurnkey } from "@turnkey/sdk-react";
export default function Dashboard() {
const { turnkey, getActiveClient } = useTurnkey();
const [user, setUser] = useState<User | null>(null);
const getSignWith = async () => {
// This will return the authIframeClient with the credential bundle injected
const client = await getActiveClient();
// 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> */*/);
}
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.
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",
});
};
Display
Add a button to the UI to trigger the signMessage
function.
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>
);
}
Recap
In this quickstart guide, you've learned how to:
- Set up Turnkey's SDK in a Next.js application
- Configure authentication with email sign-in
- Create a protected dashboard route
- Implement message signing functionality using a user's Turnkey wallet
- Handle user sessions and wallet interactions
Complete Code
"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 () => {
router.push("/dashboard");
};
const onError = (errorMessage: string) => {
setErrorMessage(errorMessage);
};
const config = {
authConfig: {
emailEnabled: true,
passkeyEnabled: false,
phoneEnabled: false,
appleEnabled: false,
facebookEnabled: false,
googleEnabled: false,
},
configOrder: ["email"],
onAuthSuccess: onAuthSuccess,
onError: onError,
};
return (
<div>
<Auth {...config} />
</div>
);
}
import { useState, useEffect } from "react";
import { useTurnkey } from "@turnkey/sdk-react";
import { hashMessage } from "viem";
export default function Dashboard() {
const { turnkey, getActiveClient } = useTurnkey();
const [user, setUser] = useState<User | null>(null);
const [message, setMessage] = useState("");
const getSignWith = async () => {
const client = await getActiveClient();
const organizationId = user?.organization.organizationId;
const wallets = await client?.getWallets({
organizationId,
});
const walletId = wallets?.wallets[0].walletId ?? "";
const accounts = await client?.getWalletAccounts({
organizationId,
walletId,
});
const signWith = accounts?.accounts[0].address ?? "";
return signWith;
};
const signMessage = async () => {
const payload = await hashMessage(message);
const signWith = await getSignWith();
const signature = await client?.signRawPayload({
payload,
signWith,
encoding: "PAYLOAD_ENCODING_TEXT_UTF8",
hashFunction: "HASH_FUNCTION_KECCAK256",
});
};
useEffect(() => {
if (turnkey) {
const user = turnkey.getCurrentUser();
setUser(user);
}
}, [turnkey]);
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>
);
}