> ## Documentation Index
> Fetch the complete documentation index at: https://docs.privy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrating JWT authentication from Alchemy to Privy

This guide is for developers who authenticate users with Alchemy Signer using JWT (JSON Web Token) authentication and need to migrate to Privy.

<Info>
  If you use standard auth methods (email, Google, and passkeys) with the React SDK, use the [React
  migration guide](https://docs.privy.io/recipes/migrating-embedded-wallets-from-alchemy) instead.
</Info>

<Info>
  Before starting, read the [signer migration
  overview](https://www.alchemy.com/docs/wallets/wallet-integrations/privy/signer-migration-overview)
  to confirm your account type (Modular Account v2 vs. another implementation) and connection type
  (EIP-7702 vs. ERC-4337). Apps not on MAv2, or using pure 4337, need to adjust a step in this guide
  — the overview's [Edge
  cases](https://www.alchemy.com/docs/wallets/wallet-integrations/privy/signer-migration-overview#edge-cases)
  section walks through each.
</Info>

## How JWT migration differs from standard migration

With JWT auth, you control both sides of authentication — your server generates JWTs, and both Alchemy and your new provider accept them. This simplifies the migration because:

* No user export/import needed (unless you also use other auth methods) — both providers authenticate via your JWT
* JWTs are reusable — they're not one-time-use tokens, so the same JWT can authenticate with both Alchemy and Privy in a single session
* Migration detection is your responsibility — since there's no Privy-side metadata, you track which users have migrated in your own database

## Choose your integration path

This guide covers two paths. Both share the same Privy dashboard setup (Steps 1–2a), but differ in how authentication and wallet import happen:

|                      | Client-side path                                                | Server-side path                                             |
| -------------------- | --------------------------------------------------------------- | ------------------------------------------------------------ |
| Best for             | React / React Native apps where the user's browser handles auth | Backend services where your server controls auth and wallets |
| Auth hook            | `useSubscribeToJwtAuthWithFlag` (React SDK)                     | REST API: exchange JWT for a Privy `userSigner`              |
| Wallet import        | `useImportWallet` hook (client-side)                            | `@privy-io/node` SDK or REST API `/v1/wallets/import/*`      |
| Private key exposure | Briefly in client memory during export→import                   | On your server during export→import                          |

## Step 1: Add a migration tracking field to your user database

Add a field to your user/account model to track migration status. Every existing user starts as `needs_migration = true`.

```sql theme={"system"}
ALTER TABLE users ADD COLUMN signer_migrated BOOLEAN DEFAULT FALSE;
```

Or use whatever mechanism fits your stack — a boolean field, a status enum, a separate migration table. The point is: you need a way to check on each login whether this user still needs their Alchemy wallet keys migrated.

## Step 2: Set up Privy with JWT auth

### 2a. Create a Privy app and configure JWT verification

This setup is the same regardless of whether you choose the client-side or server-side path.

1. Go to the [Privy Dashboard](https://dashboard.privy.io/) and create an app
2. Request access to Custom Auth Support in the Integrations > Plugins tab
3. Navigate to User Management > Authentication > JWT-based auth
4. Select the environment:

* Client side if JWT-authenticated requests will come from end-user devices (React / React Native)
* Server side if requests will come from your backend servers

5. Provide your JWT verification key — either a JWKS endpoint URL or a public verification key (PEM certificate)
6. Enter the JWT claim that contains the user's unique ID (usually `sub`)

See the [Privy JWT setup docs](https://docs.privy.io/authentication/user-authentication/jwt-based-auth/setup) for full details.

### 2b. Disable automatic wallet creation during migration

While existing users still need migrating, you don't want Privy to auto-create a fresh wallet before you import their Alchemy wallet. If using a client SDK, set `createOnLogin: "off"`:

```tsx theme={"system"}
<PrivyProvider
  appId="YOUR_PRIVY_APP_ID"
  config={{
    embeddedWallets: {
      createOnLogin: "off",
    },
  }}
>
```

For new users (signing up after your migration is live): because you track migration status in your own database (Step 1), new users start with `signer_migrated = true` (no Alchemy wallet to migrate). Create a Privy wallet for them explicitly after login — either by calling [`createWallet`](https://docs.privy.io/wallets/using-wallets/ethereum/create-a-wallet) from the client SDK or via the `@privy-io/node` SDK server-side. Once all existing users have migrated, switch `createOnLogin` back to `"users-without-wallets"` and you can drop the explicit creation call.

## Step 3: Reconnect sending and gas sponsorship

Install `@alchemy/wallet-apis` and wire up `createSmartWalletClient`. See the [Privy signer integration guide](https://www.alchemy.com/docs/wallets/third-party/signers/privy) for full setup details, or follow the [React migration guide Step 2](https://docs.privy.io/recipes/migrating-embedded-wallets-from-alchemy#step-2-reconnect-sending-and-gas-sponsorship) for a walkthrough.

***

## Client-side path

Use this path if you have a React or React Native app and want the migration to happen in the user's browser/device.

### 4a. Integrate Privy auth (client-side)

Instead of calling a Privy login function directly, you subscribe the Privy SDK to your existing auth provider's state using the `useSubscribeToJwtAuthWithFlag` hook. Privy will automatically authenticate when your provider reports the user is logged in.

In a component that lives below both `PrivyProvider` and your auth provider:

```tsx theme={"system"}
import {useAuth} from 'your-auth-provider';
import {useSubscribeToJwtAuthWithFlag} from '@privy-io/react-auth';

const AuthStateSync = () => {
  const {getToken, isLoading, isAuthenticated} = useAuth();

  useSubscribeToJwtAuthWithFlag({
    isAuthenticated,
    isLoading,
    getExternalJwt: async () => {
      if (isAuthenticated) {
        return await getToken();
      }
    }
  });

  return null;
};
```

Mount this component throughout the lifetime of your app to keep Privy in sync:

```tsx theme={"system"}
import {AuthProvider} from 'your-auth-provider';
import {PrivyProvider} from '@privy-io/react-auth';

function App() {
  return (
    <AuthProvider>
      <PrivyProvider appId="YOUR_PRIVY_APP_ID" config={{embeddedWallets: {createOnLogin: 'off'}}}>
        <AuthStateSync />
        <MainContent />
      </PrivyProvider>
    </AuthProvider>
  );
}
```

You can check the user's Privy auth status with `usePrivy`:

```tsx theme={"system"}
import {usePrivy} from '@privy-io/react-auth';

function MainContent() {
  const {user, ready, authenticated} = usePrivy();

  if (!ready) return <div>Loading...</div>;
  if (!authenticated) return <div>Please log in through your authentication provider</div>;

  return <div>Welcome, {user.id}!</div>;
}
```

<Warning>
  Do not call Privy's `login` method (from `useLogin` or `usePrivy`) when using JWT-based auth. Let
  your auth provider handle login; Privy syncs automatically.
</Warning>

See the [Privy JWT usage docs](https://docs.privy.io/authentication/user-authentication/jwt-based-auth/usage) for full details.

### 4b. Build the client-side migration flow

The migration happens in a single session where the user is authenticated with both Alchemy and Privy simultaneously.

```tsx theme={"system"}
// useImportWallet is a React hook — call it at the component/hook level, not inside async functions
const {importWallet} = useImportWallet();

async function handleMigration(userId: string) {
  // 1. Get JWT from your auth provider (same token Privy is already using)
  const jwt = await getToken();

  // 2. Check if user needs migration
  const user = await getUserFromDB(userId);
  if (!user.signer_migrated) {
    // 3. Authenticate with Alchemy using the SAME JWT
    const alchemySigner = new AlchemyWebSigner({
      client: {
        connection: {apiKey: 'YOUR_ALCHEMY_API_KEY'},
        iframeConfig: {iframeContainerId: 'alchemy-signer-iframe-container'}
      }
    });
    await alchemySigner.authenticate({type: 'jwt', token: jwt});

    // 4. Export private key from Alchemy
    const privateKey = await alchemySigner.exportPrivateKey();

    // 5. Import into Privy
    await importWallet({privateKey});

    // 6. Mark migration complete
    await updateUserDB(userId, {signer_migrated: true});
  }
}
```

### 4c. Export details

The snippet in 4b already authenticates with Alchemy using the same JWT and calls `exportPrivateKey()`. A note on when to use the encrypted variant:

```tsx theme={"system"}
// Plaintext export — fine for the fully client-side flow above
const privateKey = await alchemySigner.exportPrivateKey();

// If you need to ship the key to another process (e.g. your backend for the server-side path),
// export it encrypted against a public key you control:
const privateKeyEncrypted = await alchemySigner.exportPrivateKeyEncrypted(...);
```

### 4d. Import into Privy (client-side)

Use the `useImportWallet` hook from `@privy-io/react-auth`. It accepts a raw hex private key (with or without `0x` prefix) for Ethereum, or a base58-encoded key for Solana.

Ethereum:

```tsx theme={"system"}
import {useImportWallet} from '@privy-io/react-auth';

const {importWallet} = useImportWallet();
const wallet = await importWallet({privateKey: ethPrivateKey});
```

Solana:

```tsx theme={"system"}
import {useImportWallet} from '@privy-io/react-auth/solana';

const {importWallet} = useImportWallet();
const wallet = await importWallet({privateKey: solPrivateKey});
```

### 4e. Mark migration complete

Update your database so the user isn't prompted again on next login.

```tsx theme={"system"}
await db.users.update(userId, {signer_migrated: true});
```

***

## Server-side path (import only)

<Warning>
  Alchemy's JWT signer runs in the browser via `AlchemyWebSigner` — there is no supported path for authenticating an Alchemy JWT signer from a backend. That means the export from Alchemy must still happen client-side (as in 4a–4c above). This server-side path only covers the import side: your client exports the key from Alchemy, ships it to your server, and your server imports it into Privy using the `@privy-io/node` SDK.

  For most apps, the fully client-side path in 4a–4e is simpler. Use this path only if you have a specific reason to move the import to the server — for example, to attach policies, owners, or additional signers at import time.
</Warning>

### 5a. Set up the Privy Node SDK

In the Privy dashboard JWT configuration (Step 2a), select "Server side" so Privy will accept JWTs from your backend. Then install the SDK:

```tsx theme={"system"}
import {PrivyClient} from '@privy-io/node';

const privy = new PrivyClient({
  appId: 'your-privy-app-id',
  appSecret: 'your-privy-app-secret'
});
```

### 5b. Ship the exported key to your server

On the client, export the private key from Alchemy as in 4c. To avoid sending plaintext key material over the network, use `exportPrivateKeyEncrypted` with a public key owned by your backend:

```tsx theme={"system"}
// Client
const encryptedKey = await alchemySigner.exportPrivateKeyEncrypted({
  publicKey: serverPublicKey
});
await fetch('/api/migrate-signer', {
  method: 'POST',
  body: JSON.stringify({encryptedKey})
});
```

### 5c. Import into Privy from your backend

Decrypt the key with your server's private key, then pass it to the Node SDK. `@privy-io/node` re-encrypts the key with HPKE before sending it to Privy's TEE, so the plaintext key never leaves your server over the wire.

```tsx theme={"system"}
async function handleMigrateSigner(userId: string, encryptedKey: string) {
  const user = await getUserFromDB(userId);
  if (user.signer_migrated) return;

  const privateKey = decryptWithServerKey(encryptedKey);

  const wallet = await privy.wallets().import({
    wallet: {
      entropy_type: 'private-key',
      chain_type: 'ethereum',
      address: user.wallet_address,
      private_key: privateKey
    }
  });

  await db.users.update(userId, {signer_migrated: true});
}
```

### 5c. Import into Privy (server-side)

The `@privy-io/node` SDK encrypts the key material automatically for secure transmission to the TEE.

Ethereum (raw private key):

```tsx theme={"system"}
const wallet = await privy.wallets().import({
  wallet: {
    entropy_type: 'private-key',
    chain_type: 'ethereum',
    address: '<wallet-address>',
    private_key: '<hex-encoded-private-key>'
  }
});
```

Solana (raw private key):

```tsx theme={"system"}
const wallet = await privy.wallets().import({
  wallet: {
    entropy_type: 'private-key',
    chain_type: 'solana',
    address: '<wallet-address>',
    private_key: '<base58-encoded-private-key>'
  }
});
```

HD wallet (mnemonic):

If you're migrating HD wallets with a BIP39 mnemonic:

```tsx theme={"system"}
const wallet = await privy.wallets().import({
  wallet: {
    entropy_type: 'hd',
    chain_type: 'ethereum',
    address: '<wallet-address>',
    private_key: '<bip39-mnemonic>',
    index: 0
  }
});
```

You can optionally assign an owner, policies, or additional signers at import time:

```tsx theme={"system"}
const wallet = await privy.wallets().import({
  wallet: {
    entropy_type: 'private-key',
    chain_type: 'ethereum',
    address: '<wallet-address>',
    private_key: '<hex-encoded-private-key>'
  },
  owner_id: '<privy-user-id>',
  policy_ids: ['<policy-id>'],
  additional_signers: [{signer_id: '<signer-id>'}]
});
```

### REST API alternative

If you're not using Node.js, use the REST API directly. The flow is three steps:

1. Initialize — call `/v1/wallets/import/init` to get an encryption public key:

```bash theme={"system"}
curl --request POST \
  --url https://api.privy.io/v1/wallets/import/init \
  --header 'Authorization: Basic <encoded-app-credentials>' \
  --header 'Content-Type: application/json' \
  --header 'privy-app-id: <privy-app-id>' \
  --data '{
    "address": "<wallet-address>",
    "chain_type": "ethereum",
    "entropy_type": "private-key",
    "encryption_type": "HPKE"
  }'
```

2. Encrypt — encrypt the private key using HPKE with the returned public key (KEM: `DHKEM_P256_HKDF_SHA256`, KDF: `HKDF_SHA256`, AEAD: `CHACHA20_POLY1305`).

3. Submit — call `/v1/wallets/import/submit` with the encrypted key:

```bash theme={"system"}
curl --request POST \
  --url https://api.privy.io/v1/wallets/import/submit \
  --header 'Authorization: Basic <encoded-app-credentials>' \
  --header 'Content-Type: application/json' \
  --header 'privy-app-id: <privy-app-id>' \
  --data '{
    "wallet": {
      "address": "<wallet-address>",
      "chain_type": "ethereum",
      "entropy_type": "private-key",
      "encryption_type": "HPKE",
      "ciphertext": "<base64-encoded-encrypted-key>",
      "encapsulated_key": "<base64-encoded-encapsulated-key>"
    }
  }'
```

See the [Privy key import docs](https://docs.privy.io/wallets/wallets/import-a-wallet/hd-wallets) for the full encryption example and HD wallet variant.

### 5d. Mark migration complete

```tsx theme={"system"}
await db.users.update(userId, {signer_migrated: true});
```

***

## Step 6: Deploy

Unlike the React migration, JWT migration does not require a user export/import step — unless your users also use other auth methods (email, Google, etc.) alongside JWT.

If JWT is your only auth method:

* Deploy your updated app
* Users authenticate via JWT with Privy going forward
* Migration happens automatically on each user's first login
* No Alchemy dashboard export needed

If you also use other auth methods:

* You still need to export users from Alchemy and import into Privy (see [React migration guide Step 4](https://docs.privy.io/recipes/migrating-embedded-wallets-from-alchemy#step-4-deploy-the-updated-app-export-users-and-import-into-privy))
* Follow the same Deploy → Export → Import sequence

### Next steps

After migration, follow the Privy guides for using embedded wallets:

* [Send a transaction](https://docs.privy.io/wallets/using-wallets/ethereum/send-a-transaction) (generic wallet usage)
* [Create authorization keys](https://docs.privy.io/controls/authorization-keys/keys/create/user/request) (for server-side usage of wallets)
* [Prepare smart wallet operations](https://www.alchemy.com/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-api-endpoints/wallet-prepare-calls)
* [Sign smart wallet operations](https://docs.privy.io/api-reference/wallets/ethereum/eth-sign-user-operation)

## Important notes

### No user export/import needed (JWT-only)

Because JWT authentication is controlled by your server, both Alchemy and Privy can verify the same JWT. There's no user metadata to transfer — your server is the source of truth for user identity. The only thing being migrated is the private key material.

### Security considerations

* Client-side path: The private key is briefly available in client memory during the export→import handoff. This is different from the React SDK which uses encrypted TEE-to-TEE transfer.
* Server-side path: The key leaves the client encrypted against your server's public key, is decrypted briefly on your server, then re-encrypted with HPKE by `@privy-io/node` before being sent to Privy's TEE. The plaintext is only briefly available on your server, never over the wire.

### Migration tracking is on you

The [React migration SDK](https://docs.privy.io/recipes/migrating-embedded-wallets-from-alchemy) detects migration need automatically via Privy metadata. In the JWT path, you own this logic entirely via your user database.

## FAQ

## Can I use the React migration SDK with JWT auth?

The React migration SDK supports standard auth methods (email, Google, passkeys). If your users authenticated exclusively via JWT, use this guide instead. If you have a mix of JWT and standard auth users, you may need both paths.

## Are JWTs one-time use?

No. JWTs are reusable until they expire. The same JWT can authenticate with both Alchemy and Privy in the same session.

## What if my JWT has a short expiration?

Ensure the JWT is valid for the duration of the migration flow (authenticate with both providers + export + import). If your JWTs expire quickly, generate a fresh one at the start of the migration flow.

## Do I need to change my JWT signing/verification setup?

You'll need to configure Privy to accept your JWTs (JWKS endpoint or public key, plus the user ID claim). Your JWT generation on the server side stays the same.

## Which path should I choose — client-side or server-side?

If you already have a React or React Native app with Alchemy's client SDK, the client-side path is the most straightforward — it mirrors the flow you already have. If your architecture is backend-driven (e.g., your server holds keys or controls wallet operations), the server-side path keeps everything on the server without requiring a client SDK integration.
