Skip to content
LogoLogo

Migrate from Privy

This guide walks you through migrating from Privy 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

Privy featureOpenfort equivalentNotes
Email OTPAuthProvider.EMAIL_OTPSimilar OTP flow
Social login (Google, Twitter)AuthProvider.GOOGLE, AuthProvider.TWITTERConfigure in dashboard
External walletsAuthProvider.WALLETWalletConnect support
Embedded wallet auto-createAutomatic via wallet configConfigured in provider
Appearance customizationuiConfig propTheme and branding

Install Openfort dependencies

Remove Privy packages and install Openfort with its peer dependencies:

Update provider configuration

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

Before (Privy):

Providers.tsx (Privy)
import { PrivyProvider } from '@privy-io/react-auth';
 
function App() {
  return (
    <PrivyProvider
      appId="your-privy-app-id"
      config={{
        appearance: {
          theme: 'dark',
          accentColor: '#6366f1',
          logo: 'https://your-logo.com/logo.png',
        },
        loginMethods: ['email', 'wallet', 'google', 'twitter'],
        embeddedWallets: {
          ethereum: {
            createOnLogin: 'users-without-wallets',
          },
        },
      }}
    >
      <YourApp />
    </PrivyProvider>
  );
}

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, polygon } from "viem/chains";
 
const config = createConfig(
  getDefaultConfig({
    appName: "Your App Name",
    chains: [mainnet, polygon],  
    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_OTP,  
              AuthProvider.GOOGLE,
              AuthProvider.TWITTER,
              AuthProvider.WALLET,
            ],
            walletRecovery: {
              defaultMethod: RecoveryMethod.AUTOMATIC,  
            },
            // Appearance configuration
            theme: "auto",  
            customTheme: {  
              '--ck-accent-color': '#6366f1',  
            },  
          }}
        >
          {children}
        </OpenfortProvider>
      </QueryClientProvider>
    </WagmiProvider>
  );
}

Update authentication code

Replace Privy authentication hooks with Openfort equivalents.

Before (Privy):

LoginButton.tsx (Privy)
import { usePrivy } from '@privy-io/react-auth';
 
function LoginButton() {
  const { ready, authenticated, user, login, logout } = usePrivy();
 
  if (!ready) return <div>Loading...</div>;
 
  if (authenticated) {
    return (
      <div>
        <p>Welcome, {user?.email?.address}</p>
        <button onClick={logout}>Logout</button>
      </div>
    );
  }
 
  return <button onClick={login}>Login with Privy</button>;
}

After (Openfort):

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

For email OTP authentication specifically:

Before (Privy):

EmailLogin.tsx (Privy)
import { useLoginWithEmail } from '@privy-io/react-auth';
 
function EmailLogin() {
  const { sendCode, loginWithCode, state } = useLoginWithEmail();
  const [email, setEmail] = useState('');
  const [code, setCode] = useState('');
 
  const handleSubmit = async () => {
    if (state.status === 'initial') {
      await sendCode({ email });
    } else if (state.status === 'awaiting-code-input') {
      await loginWithCode({ code });
    }
  };
 
  return (
    <div>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      {state.status === 'awaiting-code-input' && (
        <input value={code} onChange={(e) => setCode(e.target.value)} />
      )}
      <button onClick={handleSubmit}>
        {state.status === 'initial' ? 'Send Code' : 'Verify'}
      </button>
    </div>
  );
}

After (Openfort):

EmailLogin.tsx (Openfort)
import { useEmailOtpAuth } from "@openfort/react";  
 
function EmailLogin() {
  const {  
    requestEmailOtp,  
    signInEmailOtp,  
    isRequesting,  
    isLoading,  
    isAwaitingInput,  
  } = useEmailOtpAuth();  
  const [email, setEmail] = useState('');
  const [otp, setOtp] = useState('');
 
  const handleRequestOtp = async () => {
    await requestEmailOtp({ email });  
  };
 
  const handleSignIn = async () => {
    await signInEmailOtp({ email, otp });  
  };
 
  return (
    <div>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      {isAwaitingInput && (
        <input value={otp} onChange={(e) => setOtp(e.target.value)} />
      )}
      {!isAwaitingInput ? (
        <button onClick={handleRequestOtp} disabled={isRequesting}>
          {isRequesting ? 'Sending...' : 'Send OTP'}
        </button>
      ) : (
        <button onClick={handleSignIn} disabled={isLoading}>
          {isLoading ? 'Verifying...' : 'Verify'}
        </button>
      )}
    </div>
  );
}

Update wallet access code

Replace Privy wallet access with Wagmi hooks.

Before (Privy):

WalletInfo.tsx (Privy)
import { useWallets } from '@privy-io/react-auth';
 
function WalletInfo() {
  const { wallets } = useWallets();
  const embeddedWallet = wallets.find(w => w.walletClientType === 'privy');
 
  if (!embeddedWallet) return <p>No wallet</p>;
 
  return <p>Wallet: {embeddedWallet.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 Privy transaction hooks with Wagmi equivalents.

Before (Privy):

SendTransaction.tsx (Privy)
import { useSendTransaction } from '@privy-io/react-auth';
 
function SendTransaction() {
  const { sendTransaction } = useSendTransaction();
 
  const send = async () => {
    await sendTransaction({
      to: '0xRecipientAddress',
      value: 100000,
    });
  };
 
  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 Privy signing with Wagmi hooks.

Before (Privy):

SignMessage.tsx (Privy)
import { useSignMessage } from '@privy-io/react-auth';
 
function SignMessage() {
  const { signMessage } = useSignMessage();
 
  const sign = async () => {
    const { signature } = await signMessage({ 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 Privy dependencies

Clean up unused Privy packages:

Hook mapping reference

Privy hookOpenfort/Wagmi equivalent
usePrivy (login, logout, authenticated)useUser + useSignOut + OpenfortButton
useLoginWithEmailuseEmailOtpAuth
useWalletsuseAccount (wagmi)
useSendTransactionuseSendTransaction (wagmi)
useSignMessageuseSignMessage (wagmi)

Considerations

Embedded wallet creation

Privy's createOnLogin: 'users-without-wallets' is the default behavior in Openfort. Openfort creates wallets automatically during authentication based on your recovery method configuration.

Session handling

Privy 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 Privy wallet.
  2. Transfer assets to their new Openfort wallet after authentication.

Chain configuration

Both Privy 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],
  })
);

Smart wallet benefits

Openfort wallets are ERC-4337 smart contract accounts. After migration, you gain access to:

  • Gas sponsorship: Cover gas fees for your users.
  • Transaction batching: Combine multiple operations into one transaction.
  • Session keys: Allow specific actions without user confirmation.

Configure gas sponsorship in the Openfort dashboard.

Recovery methods

Privy uses TEE-based key management. Openfort provides three recovery options:

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

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.