Skip to main content

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
preferredLanguage
'en' | 'ar'
Preferred language for the customer (default: “en”)
const customerId = await convex.mutation(api.customers.account.registerCustomer, {
  preferredLanguage: "en"
});
"c123456789"

Update Customer Profile

Update the authenticated customer’s profile details.
name
string
Display name of the customer
email
string
Email address of the customer
phoneNumber
string
Phone number of the customer
preferences.preferredLanguage
'en' | 'ar'
Preferred language
preferences.pushNotifications
boolean
Enable push notifications
preferences.emailNotifications
boolean
Enable email notifications
preferences.smsNotifications
boolean
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,
  },
});
"c123456789"

Avatar Management

Update Avatar

Upload and set a new avatar image for the customer.
avatarId
Id<'_storage'>
required
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).
username
string
required
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.
username
string
required
Username to check
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.
searchTerm
string
required
Search term to match usernames
limit
number
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).
isPrivate
boolean
required
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.
customerId
Id<'customers'>
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.
customerId
Id<'customers'>
Customer ID (defaults to authenticated user)
paginationOpts
PaginationOptions
required
Pagination configuration
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.
customerId
Id<'customers'>
Customer ID (defaults to authenticated user)
paginationOpts
PaginationOptions
required
Pagination configuration
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:
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

Status Code: 200 (Success, returns existing customer)
{
  "customerId": "c123456789",
  "message": "Customer already exists, preferences updated"
}
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.