Overview
The Customer Account API handles customer registration, profile management, and social features including following other customers and stores. This system integrates with Clerk authentication to provide comprehensive user account management.
Location: convex/customers/account.ts
Register Customer
Register a new customer or update existing customer preferences. New customers automatically get:
Auto-generated unique username
Default private account (followers only)
Profile ready for avatar upload
Preferred language for the customer (default: “en”)
TypeScript
JavaScript
Python
const customerId = await convex . mutation ( api . customers . account . registerCustomer , {
preferredLanguage: "en"
});
Update Customer Profile
Update the authenticated customer’s profile details.
Display name of the customer
Email address of the customer
Phone number of the customer
preferences.preferredLanguage
Preferred language
preferences.pushNotifications
Enable push notifications
preferences.emailNotifications
Enable email notifications
preferences.smsNotifications
Enable SMS notifications
const updatedCustomerId = await convex . mutation ( api . customers . account . updateCustomer , {
name: "John Doe" ,
email: "[email protected] " ,
phoneNumber: "+971501234567" ,
preferences: {
preferredLanguage: "en" ,
pushNotifications: true ,
emailNotifications: true ,
smsNotifications: false ,
},
});
Avatar Management
Update Avatar
Upload and set a new avatar image for the customer.
Storage ID of the uploaded avatar image
const customerId = await convex . mutation ( api . customers . account . updateAvatar , {
avatarId: "storage_abc123"
});
Remove Avatar
Remove the customer’s current avatar image.
const customerId = await convex . mutation ( api . customers . account . removeAvatar );
Username Management
Update Username
Change the customer’s username (must be unique).
New username (3-20 alphanumeric characters)
const customerId = await convex . mutation ( api . customers . account . updateUsername , {
username: "newusername"
});
Check Username Availability
Check if a username is available and get suggestions.
const availability = await convex . query ( api . customers . account . checkUsernameAvailability , {
username: "desiredusername"
});
// Response: { available: false, suggestion: "desiredusername1" }
Search Customers by Username
Search for customers by their username.
Search term to match usernames
Maximum number of results (default: 20)
const customers = await convex . query ( api . customers . account . searchCustomersByUsername , {
searchTerm: "john" ,
limit: 10
});
Privacy Controls
Update Privacy Setting
Change account privacy (private accounts show content only to followers).
Privacy setting (true = private, false = public)
const customerId = await convex . mutation ( api . customers . account . updatePrivacy , {
isPrivate: false // Make account public
});
Get Customer Profile
Retrieve customer profile information for the authenticated user or a specific customer. Respects privacy settings - private accounts are only visible to followers.
Customer ID (defaults to authenticated user)
// Get own profile
const myProfile = await convex . query ( api . customers . account . getCustomerProfile );
// Get specific customer profile
const customerProfile = await convex . query ( api . customers . account . getCustomerProfile , {
customerId: "c123456789"
});
{
"_id" : "c123456789" ,
"userId" : "user_2abc123def456" ,
"name" : "John Doe" ,
"username" : "johndoe" ,
"avatarId" : "storage_abc123" ,
"avatarUrl" : "https://storage.convex.dev/profile.jpg" ,
"email" : "[email protected] " ,
"phoneNumber" : "+971501234567" ,
"isPrivate" : true ,
"stats" : {
"totalOrders" : 25 ,
"totalSpent" : 1250.75 ,
"averageOrderValue" : 50.03 ,
"favoriteStores" : 8 ,
"reviewsWritten" : 12
},
"followersCount" : 45 ,
"followingCustomersCount" : 32 ,
"followingStoresCount" : 15 ,
"preferences" : {
"notifications" : {
"orderUpdates" : true ,
"promotions" : true ,
"newStores" : false
},
"privacy" : {
"showProfile" : true ,
"showOrders" : false ,
"showFollowers" : true
}
},
"_creationTime" : 1640995200000 ,
"lastActive" : 1640995800000
}
Get Customer Followings
Retrieve customers that the authenticated user is following.
Customer ID (defaults to authenticated user)
const followings = await convex . query ( api . customers . account . getCustomerFollowings , {
paginationOpts: { numItems: 20 , cursor: null }
});
{
"page" : [
{
"_id" : "c987654321" ,
"name" : "Jane Smith" ,
"profileImage" : "https://storage.convex.dev/jane.jpg" ,
"stats" : {
"totalOrders" : 18 ,
"reviewsWritten" : 8
},
"followedAt" : 1640995200000
},
{
"_id" : "c555666777" ,
"name" : "Mike Johnson" ,
"profileImage" : "https://storage.convex.dev/mike.jpg" ,
"stats" : {
"totalOrders" : 32 ,
"reviewsWritten" : 15
},
"followedAt" : 1640994000000
}
],
"isDone" : false ,
"continueCursor" : "eyJfaWQiOiJjNTU1NjY2Nzc3In0"
}
Get Customer Followers
Retrieve customers that follow the authenticated user.
Customer ID (defaults to authenticated user)
const followers = await convex . query ( api . customers . account . getCustomerFollowers , {
paginationOpts: { numItems: 20 , cursor: null }
});
{
"page" : [
{
"_id" : "c111222333" ,
"name" : "Sarah Wilson" ,
"profileImage" : "https://storage.convex.dev/sarah.jpg" ,
"stats" : {
"totalOrders" : 12 ,
"reviewsWritten" : 5
},
"followedAt" : 1640995800000
}
],
"isDone" : true ,
"continueCursor" : null
}
Customer Profile Component
Complete customer profile management:
React Profile Component
Customer Registration Hook
import { useQuery , useMutation } from 'convex/react' ;
import { useUser } from '@clerk/nextjs' ;
import { api } from './convex/_generated/api' ;
function CustomerProfile () {
const { user } = useUser ();
const profile = useQuery ( api . customers . account . getCustomerProfile );
const followings = useQuery ( api . customers . account . getCustomerFollowings , {
paginationOpts: { numItems: 10 , cursor: null }
});
const followers = useQuery ( api . customers . account . getCustomerFollowers , {
paginationOpts: { numItems: 10 , cursor: null }
});
if ( ! profile ) {
return < div > Loading profile ...</ div > ;
}
return (
< div className = "max-w-4xl mx-auto space-y-6" >
{ /* Profile Header */ }
< div className = "bg-white rounded-lg shadow p-6" >
< div className = "flex items-center space-x-6" >
< div className = "w-24 h-24 rounded-full overflow-hidden bg-gray-200" >
{ profile . profileImage ? (
< img
src = {profile. profileImage }
alt = {profile. name }
className = "w-full h-full object-cover"
/>
) : (
< div className = "w-full h-full flex items-center justify-center text-gray-500 text-2xl" >
{ profile . name ?. charAt (0) || '?'}
</ div >
)}
</ div >
< div className = "flex-1" >
< h1 className = "text-2xl font-bold text-gray-900" > {profile. name } </ h1 >
< p className = "text-gray-600" > {profile. email } </ p >
< p className = "text-gray-600" > {profile. phoneNumber } </ p >
< div className = "flex items-center space-x-4 mt-2" >
< span className = "text-sm text-gray-500" >
Member since { new Date ( profile . _creationTime ). toLocaleDateString ()}
</ span >
< span className = { `px-2 py-1 rounded-full text-xs font-medium ${
profile . preferredLanguage === 'en'
? 'bg-blue-100 text-blue-800'
: 'bg-green-100 text-green-800'
} ` } >
{ profile . preferredLanguage === ' en ' ? 'English' : 'العربية' }
</ span >
</ div >
</ div >
</ div >
</ div >
{ /* Stats Grid */ }
< div className = "grid grid-cols-1 md:grid-cols-4 gap-4" >
< div className = "bg-white p-4 rounded-lg shadow text-center" >
< p className = "text-2xl font-bold text-blue-600" > {profile.stats. totalOrders } </ p >
< p className = "text-sm text-gray-600" > Total Orders </ p >
</ div >
< div className = "bg-white p-4 rounded-lg shadow text-center" >
< p className = "text-2xl font-bold text-green-600" > $ {profile.stats.totalSpent.toFixed( 2 ) } </ p >
< p className = "text-sm text-gray-600" > Total Spent </ p >
</ div >
< div className = "bg-white p-4 rounded-lg shadow text-center" >
< p className = "text-2xl font-bold text-purple-600" > {profile.stats. reviewsWritten } </ p >
< p className = "text-sm text-gray-600" > Reviews Written </ p >
</ div >
< div className = "bg-white p-4 rounded-lg shadow text-center" >
< p className = "text-2xl font-bold text-orange-600" > {profile.stats. favoriteStores } </ p >
< p className = "text-sm text-gray-600" > Favorite Stores </ p >
</ div >
</ div >
{ /* Social Stats */ }
< div className = "grid grid-cols-1 md:grid-cols-3 gap-4" >
< div className = "bg-white p-4 rounded-lg shadow text-center" >
< p className = "text-xl font-bold text-gray-900" > {profile.socialStats. followersCount } </ p >
< p className = "text-sm text-gray-600" > Followers </ p >
</ div >
< div className = "bg-white p-4 rounded-lg shadow text-center" >
< p className = "text-xl font-bold text-gray-900" > {profile.socialStats. followingCount } </ p >
< p className = "text-sm text-gray-600" > Following </ p >
</ div >
< div className = "bg-white p-4 rounded-lg shadow text-center" >
< p className = "text-xl font-bold text-gray-900" > {profile.socialStats. storesFollowingCount } </ p >
< p className = "text-sm text-gray-600" > Stores Following </ p >
</ div >
</ div >
{ /* Following List */ }
{ followings && followings . page . length > 0 && (
< div className = "bg-white rounded-lg shadow p-6" >
< h2 className = "text-lg font-semibold mb-4" > Following ({profile.socialStats. followingCount }) </ h2 >
< div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
{ followings . page . map (( customer ) => (
< div key = {customer. _id } className = "flex items-center space-x-3 p-3 bg-gray-50 rounded" >
< div className = "w-10 h-10 rounded-full overflow-hidden bg-gray-200" >
{ customer . profileImage ? (
< img
src = {customer. profileImage }
alt = {customer. name }
className = "w-full h-full object-cover"
/>
) : (
< div className = "w-full h-full flex items-center justify-center text-gray-500 text-sm" >
{ customer . name ?. charAt (0) || '?'}
</ div >
)}
</ div >
< div className = "flex-1 min-w-0" >
< p className = "font-medium text-gray-900 truncate" > {customer. name } </ p >
< p className = "text-sm text-gray-600" >
{ customer . stats . totalOrders } orders • { customer . stats . reviewsWritten } reviews
</ p >
</ div >
</ div >
))}
</ div >
{! followings . isDone && (
< button className = "mt-4 text-blue-600 hover:text-blue-800" >
Load More
</ button >
)}
</ div >
)}
{ /* Followers List */ }
{ followers && followers . page . length > 0 && (
< div className = "bg-white rounded-lg shadow p-6" >
< h2 className = "text-lg font-semibold mb-4" > Followers ({profile.socialStats. followersCount }) </ h2 >
< div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
{ followers . page . map (( customer ) => (
< div key = {customer. _id } className = "flex items-center space-x-3 p-3 bg-gray-50 rounded" >
< div className = "w-10 h-10 rounded-full overflow-hidden bg-gray-200" >
{ customer . profileImage ? (
< img
src = {customer. profileImage }
alt = {customer. name }
className = "w-full h-full object-cover"
/>
) : (
< div className = "w-full h-full flex items-center justify-center text-gray-500 text-sm" >
{ customer . name ?. charAt (0) || '?'}
</ div >
)}
</ div >
< div className = "flex-1 min-w-0" >
< p className = "font-medium text-gray-900 truncate" > {customer. name } </ p >
< p className = "text-sm text-gray-600" >
Following since { new Date ( customer . followedAt ). toLocaleDateString ()}
</ p >
</ div >
</ div >
))}
</ div >
</ div >
)}
</ div >
);
}
export default CustomerProfile ;
Customer Onboarding Flow
Complete onboarding process for new customers:
import { useState } from 'react' ;
import { useUser } from '@clerk/nextjs' ;
import { useMutation } from 'convex/react' ;
import { api } from './convex/_generated/api' ;
interface OnboardingProps {
onComplete : () => void ;
}
function CustomerOnboarding ({ onComplete } : OnboardingProps ) {
const { user } = useUser ();
const [ step , setStep ] = useState ( 1 );
const [ preferences , setPreferences ] = useState ({
preferredLanguage: 'en' as 'en' | 'ar' ,
notifications: {
orderUpdates: true ,
promotions: true ,
newStores: false
}
});
const [ loading , setLoading ] = useState ( false );
const registerCustomer = useMutation ( api . customers . account . registerCustomer );
const handleComplete = async () => {
if ( ! user ) return ;
setLoading ( true );
try {
// Register customer
const customerId = await registerCustomer ({
preferredLanguage: preferences . preferredLanguage
});
// Update Clerk metadata
await user . update ({
publicMetadata: {
role: 'customer' ,
customerId: customerId ,
onboardingCompleted: true
}
});
onComplete ();
} catch ( error ) {
console . error ( 'Onboarding failed:' , error );
} finally {
setLoading ( false );
}
};
return (
< div className = "max-w-md mx-auto bg-white p-6 rounded-lg shadow" >
< div className = "text-center mb-6" >
< h2 className = "text-2xl font-bold text-gray-900" > Welcome to Twigz ! 🎉 </ h2 >
< p className = "text-gray-600 mt-2" > Let 's set up your account</p >
</ div >
{ /* Progress Indicator */ }
< div className = "flex justify-center mb-6" >
< div className = "flex space-x-2" >
{[1, 2, 3]. map (( stepNum ) => (
< div
key = { stepNum }
className = { `w-3 h-3 rounded-full ${
step >= stepNum ? 'bg-blue-600' : 'bg-gray-300'
} ` }
/>
))}
</ div >
</ div >
{ /* Step Content */ }
{ step === 1 && (
< div className = "space-y-4" >
< h3 className = "font-semibold" > Choose Your Language </ h3 >
< div className = "space-y-2" >
< label className = "flex items-center space-x-3" >
< input
type = "radio"
name = "language"
value = "en"
checked = {preferences. preferredLanguage === 'en' }
onChange = {(e) => setPreferences ( prev => ({
... prev ,
preferredLanguage: e . target . value as 'en'
}))}
className = "text-blue-600"
/>
< span > English </ span >
</ label >
< label className = "flex items-center space-x-3" >
< input
type = "radio"
name = "language"
value = "ar"
checked = {preferences. preferredLanguage === 'ar' }
onChange = {(e) => setPreferences ( prev => ({
... prev ,
preferredLanguage: e . target . value as 'ar'
}))}
className = "text-blue-600"
/>
< span > العربية ( Arabic )</ span >
</ label >
</ div >
</ div >
)}
{ step === 2 && (
< div className = "space-y-4" >
< h3 className = "font-semibold" > Notification Preferences </ h3 >
< div className = "space-y-3" >
< label className = "flex items-center justify-between" >
< span > Order Updates </ span >
< input
type = "checkbox"
checked = {preferences.notifications. orderUpdates }
onChange = {(e) => setPreferences ( prev => ({
... prev ,
notifications: {
... prev . notifications ,
orderUpdates: e . target . checked
}
}))}
className = "text-blue-600"
/>
</ label >
< label className = "flex items-center justify-between" >
< span > Promotions & Offers </ span >
< input
type = "checkbox"
checked = {preferences.notifications. promotions }
onChange = {(e) => setPreferences ( prev => ({
... prev ,
notifications: {
... prev . notifications ,
promotions: e . target . checked
}
}))}
className = "text-blue-600"
/>
</ label >
< label className = "flex items-center justify-between" >
< span > New Store Alerts </ span >
< input
type = "checkbox"
checked = {preferences.notifications. newStores }
onChange = {(e) => setPreferences ( prev => ({
... prev ,
notifications: {
... prev . notifications ,
newStores: e . target . checked
}
}))}
className = "text-blue-600"
/>
</ label >
</ div >
</ div >
)}
{ step === 3 && (
< div className = "text-center space-y-4" >
< div className = "w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto" >
< svg className = "w-8 h-8 text-green-600" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M5 13l4 4L19 7" />
</ svg >
</ div >
< h3 className = "font-semibold" > You 're All Set!</h3 >
< p className = "text-gray-600" >
Your account is ready . Start exploring amazing stores and products !
</ p >
</ div >
)}
{ /* Navigation Buttons */ }
< div className = "flex justify-between mt-6" >
{ step > 1 && step < 3 && (
< button
onClick = {() => setStep ( step - 1)}
className = "px-4 py-2 text-gray-600 hover:text-gray-800"
>
Back
</ button >
)}
< div className = "ml-auto" >
{ step < 3 ? (
< button
onClick = {() => setStep ( step + 1)}
className = "px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Next
</ button >
) : (
< button
onClick = { handleComplete }
disabled = { loading }
className = "px-6 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
>
{ loading ? 'Setting up...' : 'Get Started' }
</ button >
)}
</ div >
</ div >
</ div >
);
}
export default CustomerOnboarding ;
Social Features
The account system includes social networking features:
Follow Customers Follow other customers to see their activity in your feed and discover new products through their interactions.
Follow Stores Follow favorite stores to get notifications about new products, promotions, and special offers.
Activity Feed See activity from followed customers and stores including new products, reviews, and recommendations.
Social Discovery Discover new stores and products through your social network and trending content.
Privacy Settings
Customers can control their privacy preferences:
interface CustomerPrivacySettings {
showProfile : boolean ; // Profile visible to other users
showOrders : boolean ; // Order history visible to followers
showFollowers : boolean ; // Follower list visible to others
showActivity : boolean ; // Activity visible in feeds
allowMessages : boolean ; // Allow direct messages
showLocation : boolean ; // Show general location (city/area)
}
Error Handling
Customer already registered
Status Code : 200 (Success, returns existing customer){
"customerId" : "c123456789" ,
"message" : "Customer already exists, preferences updated"
}
Invalid language preference
Status Code : 400{
"error" : "Invalid language preference" ,
"supportedLanguages" : [ "en" , "ar" ]
}
Status Code : 404{
"error" : "Customer profile not found" ,
"customerId" : "c123456789"
}
Customer registration is idempotent - calling it multiple times for the same user will update preferences rather than create duplicate accounts.
Use the auto-registration pattern to seamlessly onboard users when they first authenticate, then guide them through preference setup.