ADR: Privy Migration for EmProps Studio + Unified User Table
Date: 2026-02-20 Status: Proposed Author: Architecture Team Builds On:
Context
The miniapp already has Privy integration via upstream sync (Farcaster login creates Privy accounts). The goal is to:
- Create a unified
usertable that bridges identities across apps (Privy ID, Dynamic ID, Farcaster ID) - Replace Dynamic Labs with Privy in EmProps Studio
- Migrate existing Dynamic users to Privy accounts
Current State
- EmProps Studio: Dynamic Labs SDK (
@dynamic-labs/sdk-react-corev4.25.7) with SIWE - Miniapp: Farcaster Mini App SDK, upstream branch has Privy for Farcaster login
- Monitor: Already has Privy integration (
@privy-io/react-authv3.10.0) - Database: No universal user table.
miniapp_user(Farcaster-based),profile,social_linkexist separately
Key Files (Current Dynamic Implementation)
| File | Purpose |
|---|---|
apps/emprops-studio/components/ClientOnlyDynamicProvider.tsx | Dynamic provider wrapper |
apps/emprops-studio/components/SSRSafeDynamicProvider.tsx | SSR-safe Dynamic wrapper |
apps/emprops-studio/pages/api/auth/me.ts | JWT validation (reconstructs chunked cookies) |
apps/emprops-studio/utils/cookies.ts | Chunked cookie read/write for large Dynamic JWTs |
apps/emprops-studio/hooks/wallets.ts | Wallet CRUD hooks (Dynamic-specific) |
apps/monitor/src/components/AuthProvider.tsx | Reference Privy implementation |
Decision
Phase 1: Database - Unified user Table
Based on the universal user table ADR, but simplified for immediate needs:
model user {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
privy_id String? @unique // Privy DID (e.g., "did:privy:abc123")
dynamic_id String? @unique // Dynamic Labs user ID (for migration)
wallet_address String? // Primary wallet address
email String?
display_name String?
avatar_url String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
migrated_at DateTime? // When Dynamic->Privy migration completed
miniapp_user miniapp_user?
@@index([wallet_address])
@@index([email])
}Add nullable FK to miniapp_user:
model miniapp_user {
// ... existing fields ...
user_id String? @unique @db.Uuid
user user? @relation(fields: [user_id], references: [id])
}Why UUID: Matches every other table in the schema. Existing user_id columns across 20+ tables are already @db.Uuid, so FK constraints can be added without column type changes. The privy_id column handles provider-specific lookups.
Migration: npx prisma migrate dev --name add_user_table - table starts empty, populated as users authenticate via Privy or get batch-imported.
Phase 2: EmProps Studio - Replace Dynamic with Privy
Dependencies:
Remove: @dynamic-labs/sdk-react-core, @dynamic-labs/ethereum
Add: @privy-io/react-auth, @privy-io/server-authNew Auth Provider (following monitor's AuthProvider.tsx pattern):
// apps/emprops-studio/components/PrivyAuthProvider.tsx
<PrivyProvider
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID}
config={{
appearance: { theme: "dark" },
loginMethods: ["wallet", "email"],
embeddedWallets: { ethereum: { createOnLogin: "users-without-wallets" } }
}}
>
{children}
</PrivyProvider>Auth Hook Abstraction:
// apps/emprops-studio/hooks/useAuth.ts
export function useAuth() {
const { ready, authenticated, user, login, logout, getAccessToken } = usePrivy();
return {
ready,
authenticated,
user: user ? {
id: user.id,
email: user.email?.address,
walletAddress: user.wallet?.address,
} : null,
login,
logout,
getAccessToken,
};
}API Route Updates:
pages/api/auth/me.ts: Replace Dynamic JWT reconstruction from chunked cookies with Privy token validation via@privy-io/server-auth. On successful auth, upsertusertable withprivy_id.- Remove
writeChunkedCookies()/readChunkedCookies()- Privy handles its own session.
Environment Variables:
- Add:
NEXT_PUBLIC_PRIVY_APP_ID,PRIVY_APP_SECRET - Remove:
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID
Phase 3: User Migration (Dynamic -> Privy)
Strategy: Just-in-Time + Batch Import
Just-in-Time (Primary):
- Existing user logs into EmProps Studio with Privy (same wallet)
- Backend detects wallet address matches a known Dynamic user
- Creates
userrecord linking bothprivy_idanddynamic_id - Seamless transition
Batch Import (Backfill) via Privy API:
POST https://auth.privy.io/api/v1/users/import
Authorization: Basic base64(PRIVY_APP_ID:PRIVY_APP_SECRET)
{
"users": [
{
"create_ethereum_wallet": false,
"linked_accounts": [
{ "type": "wallet", "chain_type": "ethereum", "address": "0x..." }
]
}
]
}- Max 20 users per request
- Import by wallet address (primary identifier from Dynamic)
- Returns Privy user IDs to store in
user.privy_id
Migration Script (scripts/migrate-dynamic-to-privy.ts):
- Query all unique wallet addresses from existing user-related tables
- Batch import to Privy (20 at a time)
- Create
userrecords with bothdynamic_idandprivy_id
Backfill Miniapp Users:
- Query
miniapp_userrecords with Farcaster IDs - Look up corresponding Privy accounts (by FID or wallet address)
- Create
userrecords and link viaminiapp_user.user_id
Phase 4: Deprecate social_link Table (Future)
Once user table is established:
usertable becomes canonical identitysocial_linkdata modeled as Privy linked accounts- Gradually migrate queries from
social_linktouser+ Privy - Eventually archive
social_link
Note: This is a future phase - user coexists with social_link initially.
Implementation Order
- Database migration - Add
usertable +miniapp_user.user_idFK - EmProps Studio Privy swap - Replace Dynamic with Privy provider
- Auth API routes - Privy JWT validation + user upsert logic
- JIT migration logic - Wallet-matching for returning Dynamic users
- Batch import script - Pre-create Privy accounts for existing users
- Backfill miniapp links - Connect
miniapp_userrecords tousertable - Cleanup - Remove Dynamic SDK, chunked cookies, old env vars
Consequences
Positive
- Single user identity across miniapp and studio
- Privy handles wallet + email + Farcaster identities in one provider
- Privy was acquired by Stripe - strong long-term viability
- Monitor already uses Privy - consolidates on one auth provider
Negative
- Migration effort for existing Dynamic users
- Batch import has rate limits (20 users/request)
- Temporary dual-auth period during rollout
Risks
- Dynamic users who don't return won't get migrated (batch import mitigates)
- Wallet address matching may miss users who used different wallets
- Privy API changes could affect batch import
Verification
npx prisma migrate dev- verifyusertable with autoincrement ID- Fresh Privy login in EmProps Studio creates
userrecord - Returning Dynamic user with same wallet gets linked
userrecord - Batch import script creates Privy accounts on staging
miniapp_user.user_idcorrectly links touserafter backfill- All studio features (collections, jobs, credits) work with new auth
