Understanding Convex + Better Auth dual-database architecture. Use when: (1) "User not found" errors during login/password reset but user appears to exist, (2) users exist in app's users table but can't authenticate, (3) need to create admin users in production, (4) debugging auth flows in Convex + Better Auth setup. Better Auth stores users in component tables separate from app tables.
When using Better Auth with Convex, users exist in TWO separate locations:
betterAuth.user, betterAuth.account, betterAuth.sessionusers table in the main schemaThis causes confusing errors like "User not found" when the user exists in one location but not the other.
users table_components/betterAuth/user/documents.jsonl separate from users/documents.jsonl┌─────────────────────────────────────────┐
│ Better Auth Component │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ user table │ │ account table │ │
│ │ - email │ │ - password hash │ │
│ │ - name │ │ - providerId │ │
│ │ - verified │ │ - userId │ │
│ └─────────────┘ └──────────────────┘ │
└─────────────────────────────────────────┘
│
│ Login/Auth happens here
▼
┌─────────────────────────────────────────┐
│ App's users table │
│ - email │
│ - name │
│ - isAdmin │
│ - subscriptionStatus │
│ - (business-specific fields) │
└─────────────────────────────────────────┘
Synced via syncFromAuth mutation
Authentication uses Better Auth tables ONLY
betterAuth.user and betterAuth.accountbetterAuth.accountbetterAuth.sessionApp's users table is for business logic
syncFromAuthComponent tables can't be directly accessed
betterAuth.userconvex import --tablecurl -X POST "https://yourapp.com/api/auth/sign-up/email" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "your-password",
"name": "Admin User"
}'
// convex/adminSetup.ts
export const createAdminUser = mutation({
args: { email: v.string(), name: v.string(), setupSecret: v.string() },
handler: async (ctx, args) => {
// Verify secret
if (args.setupSecret !== process.env.ADMIN_SECRET) {
throw new Error("Unauthorized");
}
const userId = await ctx.db.insert("users", {
email: args.email,
name: args.name,
isAdmin: true,
subscriptionStatus: "ACTIVE",
});
return { userId };
},
});
npx convex run --prod adminSetup:createAdminUser \
'{"email": "[email protected]", "name": "Admin", "setupSecret": "your-secret"}'
Since you can't access component tables directly:
Export production data
npx convex export --prod --path /tmp/convex-export
Extract and modify
unzip /tmp/convex-export -d /tmp/convex-data
# Edit _components/betterAuth/user/documents.jsonl
# Edit _components/betterAuth/account/documents.jsonl
Reimport
# Recreate zip with modifications
cd /tmp/convex-data && zip -r ../convex-modified.zip .
npx convex import --prod --replace-all -y /tmp/convex-modified.zip
users/documents.jsonl AND _components/betterAuth/user/documents.jsonlsyncFromAuth pattern is common: on first login, copy Better Auth user to app tablenpx convex env set) not just .env.localbetterAuth.account use format salt:hash (scrypt-based)