Skip to content
LogoLogo

Migrate from Turnkey

This guide walks you through migrating from Turnkey to Openfort. The migration involves updating your SDK dependencies, provider configuration, authentication flows, and wallet interactions.

Before you begin

Before starting the migration:

  1. Complete the Openfort quickstart to understand the SDK structure.
  2. Obtain your API keys from the Openfort dashboard.
  3. Set up a recovery endpoint (for automatic recovery).

Feature mapping

Turnkey featureOpenfort equivalentNotes
Email OTP authenticationAuthProvider.EMAILOTP-based authentication
Social login (Google, Apple)AuthProvider.GOOGLE, AuthProvider.APPLEConfigure in dashboard
External walletsAuthProvider.WALLETWalletConnect support
handleLogin modalOpenfortButtonPre-built UI component

Install Openfort dependencies

Remove Turnkey packages and install Openfort with its peer dependencies:

Update provider configuration

Replace the Turnkey provider with Openfort's layered provider structure.

Before (Turnkey):

Providers.tsx (Turnkey)
import { TurnkeyProvider } from "@turnkey/react-wallet-kit";
import "@turnkey/react-wallet-kit/styles.css";
 
const turnkeyConfig = {
  organizationId: process.env.NEXT_PUBLIC_ORGANIZATION_ID!,
  authProxyConfigId: process.env.NEXT_PUBLIC_AUTH_PROXY_CONFIG_ID!,
  auth: {
    oauthConfig: {
      googleClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
      appleClientId: process.env.NEXT_PUBLIC_APPLE_CLIENT_ID,
    },
  },
};
 
function App() {
  return (
    <TurnkeyProvider
      config={turnkeyConfig}
      callbacks={{
        onAuthenticationSuccess: ({ session }) => {
          console.log("Authenticated:", session);
        },
      }}
    >
      <YourApp />
    </TurnkeyProvider>
  );
}

After (Openfort):

Providers.tsx (Openfort)
import {
  AuthProvider,
  OpenfortProvider,
  getDefaultConfig,
  RecoveryMethod,
} from "@openfort/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider, createConfig } from "wagmi";
import { mainnet, sepolia } from "viem/chains";
 
const config = createConfig(
  getDefaultConfig({
    appName: "Your App Name",
    chains: [mainnet, sepolia],  
    ssr: true,
  })
);
 
const queryClient = new QueryClient();
 
function Providers({ children }: { children: React.ReactNode }) {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <OpenfortProvider
          publishableKey="YOUR_OPENFORT_PUBLISHABLE_KEY"
          walletConfig={{
            shieldPublishableKey: "YOUR_SHIELD_PUBLISHABLE_KEY",  
            createEncryptedSessionEndpoint: "YOUR_RECOVERY_ENDPOINT",  
          }}
          uiConfig={{
            authProviders: [
              AuthProvider.EMAIL,
              AuthProvider.GOOGLE,
              AuthProvider.WALLET,
            ],
            walletRecovery: {
              defaultMethod: RecoveryMethod.AUTOMATIC,  
            },
          }}
        >
          {children}
        </OpenfortProvider>
      </QueryClientProvider>
    </WagmiProvider>
  );
}

Update authentication code

Replace Turnkey authentication hooks with Openfort equivalents.

Before (Turnkey):

LoginButton.tsx (Turnkey)
import { useTurnkey, AuthState } from "@turnkey/react-wallet-kit";
 
function LoginButton() {
  const { handleLogin, authState, user } = useTurnkey();
 
  if (authState === AuthState.Authenticated) {
    return (
      <div>
        <p>Welcome, {user?.userName}</p>
        <button onClick={() => window.location.reload()}>Logout</button>
      </div>
    );
  }
 
  return <button onClick={handleLogin}>Login / Sign Up</button>;
}

After (Openfort):

LoginButton.tsx (Openfort)
import { useUser, useSignOut, OpenfortButton } from "@openfort/react";
 
function LoginButton() {
  const { user, isLoading } = useUser();
  const { signOut } = useSignOut();
 
  if (isLoading) return <div>Loading...</div>;
 
  if (user) {
    return (
      <div>
        <p>Welcome, {user.email}</p>
        <button onClick={signOut}>Sign Out</button>
      </div>
    );
  }
 
  // Use the pre-built button component
  return <OpenfortButton />;  
}

For custom authentication (email OTP or passkey):

Before (Turnkey):

CustomAuth.tsx (Turnkey)
import { useTurnkey } from "@turnkey/react-wallet-kit";
 
function CustomAuth() {
  const { loginWithPasskey, signUpWithPasskey } = useTurnkey();
 
  return (
    <div>
      <button onClick={loginWithPasskey}>Login with Passkey</button>
      <button onClick={signUpWithPasskey}>Sign Up with Passkey</button>
    </div>
  );
}

After (Openfort):

CustomLogin.tsx (Openfort)
import { useEmailOtpAuth, useOAuth, AuthProvider } from "@openfort/react";  
 
function CustomLogin() {
  const { sendOtp, verifyOtp, state } = useEmailOtpAuth();  
  const { login: googleLogin } = useOAuth();
  const [email, setEmail] = useState("");
  const [otp, setOtp] = useState("");
 
  const handleEmailLogin = async () => {
    if (state === "initial") {
      await sendOtp({ email });
    } else if (state === "awaiting_otp") {
      await verifyOtp({ otp });
    }
  };
 
  return (
    <div>
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      {state === "awaiting_otp" && (
        <input
          value={otp}
          onChange={(e) => setOtp(e.target.value)}
          placeholder="Enter OTP"
        />
      )}
      <button onClick={handleEmailLogin}>
        {state === "initial" ? "Send OTP" : "Verify"}
      </button>
      <button onClick={() => googleLogin({ provider: AuthProvider.GOOGLE })}>
        Sign in with Google
      </button>
    </div>
  );
}

Update wallet access code

Replace Turnkey wallet access with Wagmi hooks.

Before (Turnkey):

WalletInfo.tsx (Turnkey)
import { useTurnkey } from "@turnkey/react-wallet-kit";
 
function WalletInfo() {
  const { wallets } = useTurnkey();
  const wallet = wallets?.[0];
  const address = wallet?.accounts?.[0]?.address;
 
  if (!address) return <p>No wallet</p>;
 
  return <p>Wallet: {address}</p>;
}

After (Openfort):

WalletInfo.tsx (Openfort)
import { useAccount } from "wagmi";  
 
function WalletInfo() {
  const { address, isConnected } = useAccount();  
 
  if (!isConnected) return <p>No wallet</p>;
 
  return <p>Wallet: {address}</p>;  // Smart wallet address
}

Update transaction code

Replace Turnkey signing with Wagmi hooks.

Before (Turnkey):

SendTransaction.tsx (Turnkey)
import { useTurnkey } from "@turnkey/react-wallet-kit";
 
function SendTransaction() {
  const { wallets, signTransaction } = useTurnkey();
 
  const send = async () => {
    const walletAccount = wallets[0]?.accounts[0];
    if (!walletAccount) return;
 
    const signature = await signTransaction({
      walletAccount,
      unsignedTransaction: "0x...", // RLP-encoded unsigned transaction
      transactionType: "TRANSACTION_TYPE_ETHEREUM",
    });
    console.log("Transaction:", signature);
  };
 
  return <button onClick={send}>Send Transaction</button>;
}

After (Openfort):

SendTransaction.tsx (Openfort)
import { useSendTransaction, useAccount } from "wagmi";  
import { parseEther } from "viem";
 
function SendTransaction() {
  const { address } = useAccount();
  const { sendTransaction, isPending } = useSendTransaction();  
 
  const send = () => {
    sendTransaction({
      to: "0xRecipientAddress",
      value: parseEther("0.001"),
    });
  };
 
  return (
    <button onClick={send} disabled={isPending || !address}>
      {isPending ? "Sending..." : "Send Transaction"}
    </button>
  );
}

Update message signing

Replace Turnkey message signing with Wagmi hooks.

Before (Turnkey):

SignMessage.tsx (Turnkey)
import { useTurnkey } from "@turnkey/react-wallet-kit";
 
function SignMessage() {
  const { wallets, signMessage } = useTurnkey();
 
  const sign = async () => {
    const walletAccount = wallets[0]?.accounts[0];
    if (!walletAccount) return;
 
    const signature = await signMessage({
      walletAccount,
      message: "Hello World",
    });
    console.log("Signature:", signature);
  };
 
  return <button onClick={sign}>Sign Message</button>;
}

After (Openfort):

SignMessage.tsx (Openfort)
import { useSignMessage } from "wagmi";  
 
function SignMessage() {
  const { signMessage, isPending } = useSignMessage();  
 
  const sign = () => {
    signMessage({ message: "Hello World" });
  };
 
  return (
    <button onClick={sign} disabled={isPending}>
      {isPending ? "Signing..." : "Sign Message"}
    </button>
  );
}

Remove remaining Turnkey dependencies

Verify no Turnkey packages remain in your package.json:

# Check for any remaining Turnkey packages
npm ls 2>/dev/null | grep turnkey || echo "All Turnkey packages removed"

If you skipped the uninstall in step 1, remove all Turnkey packages now:

Hook mapping reference

Turnkey hook/methodOpenfort/Wagmi equivalent
useTurnkey()useUser + useAccount (wagmi)
user from useTurnkeyuseUser
handleLogin()OpenfortButton or individual auth hooks
loginWithPasskey() / signUpWithPasskey()OpenfortButton or useEmailOtpAuth, useOAuth
wallets from useTurnkeyuseAccount (wagmi)
signMessage()useSignMessage (wagmi)
signTransaction()useSendTransaction (wagmi)
authStateuseUser (isLoading, user)

Considerations

Session handling

Turnkey sessions are invalidated during migration. Users need to re-authenticate with Openfort.

Wallet addresses

Users receive new smart wallet addresses. Communicate this change to users before migration so they can:

  1. Export any assets from their Turnkey wallet.
  2. Transfer assets to their new Openfort wallet after authentication.

Key management differences

Turnkey uses a secure enclave infrastructure with Auth Proxy for key management. Openfort provides three recovery options:

Recovery methodDescriptionBackend required
AutomaticSeamless, backend-managed recoveryYes
PasswordUser-set password for recoveryNo
PasskeyBiometric/device authenticationNo

Chain configuration

Both Turnkey and Openfort support multiple chains. Update your chain configuration in the Wagmi config:

config.ts
import { mainnet, polygon, arbitrum, base } from "viem/chains";
 
const config = createConfig(
  getDefaultConfig({
    appName: "Your App",
    chains: [mainnet, polygon, arbitrum, base],
  })
);

Next steps

Wallet configuration
Wallet configuration
Wallet setup
Configure your embedded wallet settings.
Gas sponsorship
Gas sponsorship
Paymaster setup
Cover gas fees for your users.
Session keys
Session keys
Delegated signing
Delegate signing with session keys.