# Privy migration

Update imports and provider settings with the codemod, handle unsupported APIs and migrate existing users to a new wallet.

The codemod updates Privy imports and provider settings. Then address unsupported APIs in its report and verify sign-in and payments. `@shard-lab/nc-waas-react` has the Privy React SDK's names and shapes, and the server and wagmi entries keep Privy's names too. Privy wallet keys are not moved: users sign in with the same email and get a new Pouchi wallet.

## Package import

The API names match, so the imports in your app code change only the package name. The codemod makes this change too.

```diff lang="tsx"
- import { PrivyProvider, usePrivy } from "@privy-io/react-auth";
+ import { PrivyProvider, usePrivy } from "@shard-lab/nc-waas-react";
```

The provider also gets `apiUrl` and the app's chain, and `appId` takes your Pouchi app ID. [Provider setup](#provider)

## Migration steps

Before applying the codemod to your release build, preserve a transition build that can still open the old Privy wallet provider. Users need that provider to sign any asset transfers. Keep it available until the migration period ends; changing package imports alone does not move funds.

1. **Move the code with the codemod.** Look at the dry run and its report first, then apply with `--write`.

```sh
# from the nc-waas repository root
node packages/pouchi-migrate/bin/pouchi-migrate.mjs /path/to/my-app --diff     # writes nothing
node packages/pouchi-migrate/bin/pouchi-migrate.mjs /path/to/my-app --write
```

The codemod package (`@shard-lab/pouchi-migrate`) is not published yet. Today you run it from `packages/pouchi-migrate` in the nc-waas repository. Request repository and package access from [business@pouchi.app](mailto:business@pouchi.app) if you do not have them.

2. **Set up the app.** Register the app origins and the login methods you used on Privy. [App setup](#tenant)

3. **Set the env values and build.** `NEXT_PUBLIC_POUCHI_TENANT_ID`, `NEXT_PUBLIC_POUCHI_API_URL` (`VITE_` for Vite).

4. **Work through the report.** Every API Pouchi does not offer is listed with file:line and the replacement. [Support](#unsupported)

5. **Tell your users.** The same email signs in to a new wallet; users move funds from the old wallet themselves. [Existing users](#users)

## Codemod changes

| Target | Change |
| --- | --- |
| Imports | `@privy-io/react-auth` (with `/solana`, `/smart-wallets`, `/extended-chains`, `/cards`, `/farcaster`), `@privy-io/wagmi`, `@privy-io/server-auth` and `@privy-io/node` to the same subpaths of `@shard-lab/nc-waas-react`, `require()` included |
| `PrivyProvider` | Adds `apiUrl` and `config.defaultChain` (the app's chain, `marooTestnet`). Removes login methods Pouchi has no counterpart for |
| Env | `NEXT_PUBLIC_PRIVY_APP_ID` to `NEXT_PUBLIC_POUCHI_TENANT_ID`, `PRIVY_APP_ID` to `POUCHI_TENANT_ID`, `PRIVY_APP_SECRET` to `POUCHI_TENANT_CREDENTIAL`, in code and `.env*` files |
| `package.json` | The Privy packages become `@shard-lab/nc-waas-react` |
| Dev proxy | `--dev-proxy <API origin>` makes a Next.js app forward `/v1` to the Pouchi API (local development only) |

The report rules each Privy API the app uses: `ok` (same behavior), `different` (works, with a difference), `refused` (same name and type, so it compiles; a call fails with EIP-1193 `4200` and a docs link) or `missing` (not in Pouchi: the code must change). The exit code is 2 when something is `missing`.

## Provider

```diff lang="tsx"
- import { PrivyProvider } from "@privy-io/react-auth";
+ import { PrivyProvider, marooTestnet } from "@shard-lab/nc-waas-react";

  <PrivyProvider
-   appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}
+   apiUrl={process.env.NEXT_PUBLIC_POUCHI_API_URL!}
+   appId={process.env.NEXT_PUBLIC_POUCHI_TENANT_ID!}
    config={{
+     defaultChain: marooTestnet,
```

`appId` is the Pouchi app ID. The wallet lives on the app's chain only; other `supportedChains` are ignored. Privy settings such as `externalWallets`, `mfa`, `solana` and `fundingMethodConfig` are accepted, ignored, and listed in the report.

## Login methods

| Privy | Pouchi |
| --- | --- |
| Email (`useLoginWithEmail`, modal) | Same. The headless `sendCode` opens the Pouchi sheet, where a new user also protects the wallet (PIN or passkey) |
| Google, Apple (`useLoginWithOAuth`) | Supported after you enable the providers in app settings and register the redirect URI. Kakao and Naver too |
| Passkey (`useLoginWithPasskey`, `linkPasskey`) | Same. Passkey-only sign-up is refused: sign up with email or social login, then add a passkey |
| SMS, external wallets (SIWE, SIWS), Twitter, Discord, GitHub, Telegram, Farcaster, guests | Not offered. The hooks exist and a call fails with `4200`; these users sign in with email or a supported social account |
| `useOAuthTokens` | The callback never fires; a console warning says so on mount |

## wagmi

`createConfig`, `WagmiProvider` and `useSetActiveWallet` from `@privy-io/wagmi` are in `@shard-lab/nc-waas-react/wagmi`. `createConfig` adds the embedded wallet connector and the app's chain (first); `WagmiProvider` connects wagmi to the wallet on sign-in and disconnects on sign-out, so `useAccount`, `useSignMessage`, `useSignTypedData` and `useSendTransaction` act on the Pouchi wallet. `useSetActiveWallet` selects the one embedded wallet.

## Server

```diff lang="ts"
- import { PrivyClient } from "@privy-io/server-auth";
+ import { PrivyClient } from "@shard-lab/nc-waas-react/server";

  const privy = new PrivyClient(process.env.POUCHI_TENANT_ID!, process.env.POUCHI_TENANT_CREDENTIAL);
  const claims = await privy.verifyAuthToken(accessToken);
```

| Privy | Pouchi |
| --- | --- |
| `verifyAuthToken`, `utils().auth().verifyAuthToken` | Same. Offline JWKS verification, no secret needed. API origin from `POUCHI_API_URL` |
| `getUser(id)`, `getUser({ idToken })`, `users().get` | Returns the wallet and the user id, without the email (use the lookups below for it). Pass the access token as `idToken` |
| `getUserByEmail`, `getUserByWalletAddress` | Same names. Finds only this app's users and returns `null` when none matches. The email is compared with the email login (case and surrounding spaces ignored); a social account's email is not matched. Returns the user id, email, wallet address, status (including `ACTIVE_UNPROTECTED`) and sign-up time |
| `users().getByEmailAddress({ address })`, `users().getByWalletAddress({ address })` | As above; a missing user is a 404 error, as in Privy |
| `getUserByPhoneNumber`, `getUserByCustomAuthId`, `getUsers`, `users().list` | Refused. Keep that mapping to `user.id` in your database |
| `importUser`, `users().create`, `deleteUser` | Refused. Users sign up in your app and get a new wallet |

`appSecret` is the app's Client Credential written as `<credentialId>:<clientSecret>` (`wallets:read`), needed by `getUser` and the user lookups. Lookups are rate limited per credential (120 per minute by default, then `429`) and every lookup is audited.

### Webhooks

`verifyWebhook(payload, headers, secret)` and `webhooks().verify(...)` check a Pouchi webhook (`x-waas-signature: sha256=<hex>`, `x-waas-event-id`) and return the event. Pouchi event types differ from Privy's. A request with only Privy's Svix headers is refused.

### Server wallets

`walletApi`, `wallets()`, `policies()`, `keyQuorums()`, `useSigners` and `useAuthorizationSignature` are refused: Pouchi never signs for a user on the server. Use [`useSessionApproval`](/en/docs/delegation/) inside the app, or app-server payments for payments the server runs; the wallet contract enforces the limits of both.

## Support

| Privy API | Status | Instead |
| --- | --- | --- |
| `usePrivy`, `useLogin`, `useLogout`, `useUser`, `useToken`, `getAccessToken`, `useModalStatus` | Supported | `user.id` is `did:nc-waas:<tenant>:<walletId>` |
| `useWallets`, `useCreateWallet` | Supported | One EVM smart wallet per user in the current app; `createAdditional` is refused |
| `useSendTransaction`, `useSendCalls`, `useSmartWallets`, `SmartWalletsProvider` | Supported | ERC-4337; the app pays gas; the user's wallet is not charged |
| `useSignMessage`, `useSignTypedData`, `toViemAccount` | Supported | ERC-1271 (ERC-6492 before deployment); typed data `chainId` is the app's chain |
| `useLinkAccount`, `useLinkWithPasskey` | Supported | Email, Google, Apple, Kakao, Naver, passkey; others report through `onError` |
| `usePrivy().exportWallet` | Different | The split key is not exported; the account recovery method is explained instead |
| `useSessionSigners`, `useSigners`, `useAuthorizationSignature` | Rejected (`NOT_SUPPORTED`) | Use `useSessionApproval` in the app, or an approved delegated policy for server execution |
| `useIdentityToken` | Different | `identityToken` is null; the access token identifies the user |
| `useSignTransaction`, `eth_sign` | Refused | A smart wallet has no raw signed transaction; use `sendTransaction` |
| `useFundWallet`, `useAddFunds`, `useFiatOnramp`, `useDepositAddress`, `/cards` | Refused | Fund the wallet address directly (testnets: the faucet) |
| `useImportWallet`, `setWalletPassword` | Refused | No private key is ever taken; keys are created split |
| `useMfaEnrollment`, `useMfa` | Refused | Every approval already needs the device PIN or a passkey |
| `useCrossAppAccounts`, `@privy-io/cross-app-*` | Refused, missing | Each app signs its users in with Pouchi |
| `/solana`, `/extended-chains`, `useLoginWithSiws` | Refused | EVM only; wallet lists are empty |
| `@privy-io/expo`, `@privy-io/js-sdk-core` | Missing | No React Native SDK yet; without a framework use `@shard-lab/nc-waas-react/core` |

## App setup

```sh
node packages/pouchi-migrate/bin/pouchi-migrate.mjs tenant /path/to/my-app --tenant <app id> --api-url <Pouchi API> --origin https://app.example.com
```

Read-only: maps the app's `loginMethods` to Pouchi's, checks which app origins the API already allows (its CORS answer), and prints the env values and what to apply in the Pouchi console or with the developer MCP tools `set_app_origins` and `set_login_methods`.

## Existing users

Privy wallet keys are not moved. Pouchi keys are created split between the device and Pouchi and never imported. **The wallet address changes.**

1. **Same email, new wallet.** Users sign in with the email (or Google, Apple) they used on Privy and get a new Pouchi wallet at first login. Users who only had SMS or an external wallet add an email first.

2. **Link the accounts if you need to.** Send the Privy token and the Pouchi token in the same request, verify both on the server, then link them (`linkPrivyUser` in `/server`). Never link on a matching email alone.

3. **Move funds (optional).** A normal transfer from the old Privy wallet to the new Pouchi address that the user signs in the app while it still has Privy. `AssetMigrationCard` sends tokens first and the native coin last, keeping a gas reserve. This is a same-chain transfer, not a bridge. The current walkthrough supports Maroo Testnet (450815) test assets only. Check both wallets and the asset on that supported chain before sending; sending assets on another chain does not make them usable in Pouchi. The old Privy wallet must cover its own transfer gas.

4. **Update address-bound data.** Update your database and allow lists to reference the new address. NFT ownership requires a separate on-chain transfer on a supported chain; changing a database record does not transfer it. DeFi positions, token approvals and ENS names do not follow automatically.

5. **Remove the Privy SDK** after the transition; point users who did not move to Privy's wallet export.

## Post-migration checks

After applying the codemod, run the app and check in this order.

1. **Press the sign-in button.** The Pouchi sign-in sheet opens instead of the Privy modal.

2. **Sign in with the same email.** The first time, the user sets wallet protection (PIN or passkey) and gets a new wallet address.

3. **Run one transfer or signature.** The Pouchi approval screen opens, and on approval your existing code gets the same values back (`hash`, signature).

## Common errors

| Symptom | Cause | Fix |
| --- | --- | --- |
| The codemod exits with code 2 | The report has `missing` APIs (not in Pouchi), so type checking fails | Fix the code at the file:line and with the alternative in the report. [Support](#unsupported) |
| A call fails with `NOT_SUPPORTED` (`rpcCode` 4200) | A refused API such as `useFundWallet` was called | Use the link in the error message and the alternative under [Support](#unsupported) |
| `INVALID_PARAMS`: `NcWaasProvider needs chain (or config.defaultChain)` | The chain was dropped in a manual migration | Add `config.defaultChain: marooTestnet` |
| CORS error for API requests in the browser console | The app origin is not in the Pouchi app's allowed origins | Check with `pouchi-migrate tenant`, then register it in the console or with `set_app_origins` |
| Server `getUser` fails with `NOT_SUPPORTED`: `this call needs a tenant Client Credential` | No Client Credential in place of `appSecret` | Set `POUCHI_TENANT_CREDENTIAL` to `<credentialId>:<clientSecret>` (`wallets:read`) |
| An existing user has a different wallet address | Expected. Privy keys are not moved, so a new wallet is created | Follow [Existing users](#users) to link accounts and move assets |

## Official examples

All of [privy-io/examples](https://github.com/privy-io/examples) at `95a0f6e` went through the same migration. Of the 25 web and server examples, 24 type-check after the codemod (19 failed with the SDK before this work) and 21 keep no Privy package. Five built (`privy-next-starter`, `privy-next-wagmi`, `privy-react-starter`, `privy-react-whitelabel-starter`, `privy-next-fiat-onramp`), and the wagmi and whitelabel examples ran live against a local Pouchi API on Maroo Testnet: login, signing and a transfer. The score per example (same behavior 1, different 0.75, refused or missing 0) is on the [Korean page](/docs/privy-migration/).

## Next steps

- [Quickstart](/en/docs/quickstart/): transfers, server verification, wagmi
- [Delegated payments](/en/docs/delegation/): limit approvals in place of Privy server wallets
- [React SDK reference](/en/docs/reference/react/): selected public APIs and support status
- [AI development tools](/en/docs/ai/): hand the migration to a coding agent
