Implement Custom Self-Registration for a B2B Store

Implement automated customer self-registration for Salesforce B2B Commerce storefronts using a custom Lightning Web Component and an Apex controller that orchestrates user provisioning, account creation, buyer access, and permission assignment.

The self-registration solution consists of two primary components:

  • Custom web component — Provides the user-facing registration interface with form validation, user feedback, and storefront integration.
  • Custom Apex controller — Orchestrates backend operations including user provisioning, account creation, buyer access configuration, and permission management.

When to Use This Implementation 

Use this implementation when you want to:

  • Enable customers to register for B2B Commerce stores without administrator intervention.
  • Support password-based user authentication.
  • Maintain control over user provisioning and account creation workflows.
  • Enforce data validation and business rules during registration.
  • Automate buyer access setup and permission assignment.
  • Streamline the customer onboarding experience.

Customize the Self-Registration Page 

Customize the store’s registration page using custom Lightning Web Components (LWC) and Apex code. For a sample Apex class, see the CommerceSelfRegistrationController.cls class.

Before you begin:

  1. Add the new UI component to your store’s self-registration page.

    a. In the navigation sidebar, select a store from the Store dropdown.

    b. Select Website Design, and then click Experience Builder.

    c. In Experience Builder, go to the Self-registration page.

    d. To view the properties, click the page.

    e. Drag and drop the component on the page.

    f. Configure the component properties.

    g. Publish your store.

  2. Grant CommerceSelfRegistrationController.cls access to guest users.

Note: Grant guest users access only to the Apex classes required for self-registration. Don’t grant access to unrelated classes or classes that expose sensitive operations.

Don’t grant access to unrelated classes or classes that expose sensitive operations.

a. In the navigation sidebar, select a store from the Store dropdown.

b. Select Website Design, and then click Experience Builder.

c. In Experience Builder, go to Settings, and then click General.

d. Click the Guest User Profile.

e. In the Enabled Apex Class Access section, click Edit.

f. Add the Apex class to the list of enabled classes, and save your changes.

Sample Implementation of CommerceSelfRegistrationController 

The CommerceSelfRegistrationController class is the core backend component that handles all server-side operations for customer self-registration.

The following sample shows a CommerceSelfRegistrationController class implementation.

1/**
2 * Self-registration controller for Experience Cloud with B2B Commerce integration
3 */
4global without sharing class CommerceSelfRegistrationController {
5
6    private static final String LOG_PREFIX = '[CommerceSelfReg]';
7
8    public class RegistrationException extends Exception {}
9
10    global class RegistrationResult {
11        @AuraEnabled global Boolean success;
12        @AuraEnabled global String message;
13        @AuraEnabled global String redirectUrl;
14    }
15
16    /**
17     * Register a new user with BuyerAccount and auto-login
18     * Returns: RegistrationResult with success status, message, and redirect URL
19     */
20    @AuraEnabled(cacheable=false)
21    global static RegistrationResult registerUserAndRedirectForLogin(
22        String firstName,
23        String lastName,
24        String email,
25        String phoneNumber,
26        String password,
27        String confirmPassword,
28        String startUrl
29    ) {
30        RegistrationResult result = new RegistrationResult();
31        result.success = false;
32
33        Savepoint sp = Database.setSavepoint();
34
35        try {
36            String validationError = validateRegistrationInput(lastName, email, password, confirmPassword);
37            if (validationError != null) {
38                result.message = validationError;
39                return result;
40            }
41
42            String permissionSetGroupId = getPermissionSetGroupIdFromNetwork();
43
44            if (String.isBlank(startUrl)) {
45                startUrl = '';
46            }
47
48            String accountId = createBusinessAccount(firstName, lastName);
49            String userId = createUser(firstName, lastName, email, phoneNumber, password, accountId);
50
51            createBuyerAccount(accountId);
52
53            // Add buyer to all default buyer groups from config
54            List<String> defaultBuyerGroupIds = getDefaultBuyerGroupsFromConfig();
55            if (!defaultBuyerGroupIds.isEmpty()) {
56                addBuyerToDefaultGroups(accountId, defaultBuyerGroupIds);
57            }
58
59            assignPermissionSetToUserAsync(userId, permissionSetGroupId);
60            performAutoLogin(email, password, startUrl, result);
61
62        } catch (Exception ex) {
63            Database.rollback(sp);
64            result.success = false;
65            result.message = 'Registration error: ' + ex.getMessage();
66            System.debug(LOG_PREFIX + ' Registration exception: ' + ex.getMessage());
67            System.debug(LOG_PREFIX + ' Stack trace: ' + ex.getStackTraceString());
68        }
69
70        return result;
71    }
72
73    /**
74     * Validate registration input fields
75     * Returns: Error message if validation fails, null if valid
76     */
77    private static String validateRegistrationInput(String lastName, String email, String password, String confirmPassword) {
78        if (String.isBlank(lastName)) {
79            return 'Last name is required';
80        }
81
82        if (String.isBlank(email)) {
83            return 'Email is required';
84        }
85
86        if (String.isBlank(password)) {
87            return 'Password is required';
88        }
89
90        if (password != confirmPassword) {
91            return 'Passwords do not match';
92        }
93
94        return null;
95    }
96
97    /**
98     * Create external user using Site.createExternalUser()
99     * Returns: User ID
100     */
101    private static String createUser(String firstName, String lastName, String email,
102        String phoneNumber, String password, String accountId) {
103        try {
104            User u = new User();
105            u.FirstName = firstName;
106            u.LastName = lastName;
107            u.Email = email;
108            u.Username = email;
109            u.CommunityNickname = generateNickname(firstName, lastName, email);
110            u.EmailEncodingKey = 'UTF-8';
111            u.LocaleSidKey = 'en_US';
112            u.LanguageLocaleKey = 'en_US';
113            u.TimeZoneSidKey = 'America/Los_Angeles';
114            u.Alias = generateAlias(firstName, lastName);
115
116            if (!String.isBlank(phoneNumber)) {
117                u.Phone = phoneNumber;
118            }
119
120            String userId = Site.createExternalUser(u, accountId, password);
121
122            if (userId == null) {
123                throw new RegistrationException('Site.createExternalUser returned null - user creation failed');
124            }
125
126            System.debug(LOG_PREFIX + ' Created external user: ' + userId);
127            return userId;
128
129        } catch (Exception ex) {
130            System.debug(LOG_PREFIX + ' Error creating user: ' + ex.getMessage());
131            System.debug(LOG_PREFIX + ' Stack trace: ' + ex.getStackTraceString());
132            throw ex;
133        }
134    }
135
136    /**
137     * This a sample implementation and customer's need to provider their own implementation
138     * Generate unique community nickname
139     * Returns: Nickname string (max 40 chars)
140     */
141    private static String generateNickname(String firstName, String lastName, String email) {
142        String nickname = (firstName != null ? firstName : '') + (lastName != null ? lastName : '');
143        if (String.isBlank(nickname)) {
144            nickname = email.substringBefore('@');
145        }
146        nickname = nickname.replaceAll('[^a-zA-Z0-9]', '');
147        nickname = nickname + String.valueOf(System.now().getTime()).substring(7);
148        return nickname.substring(0, Math.min(nickname.length(), 40));
149    }
150
151    /**
152     * This a sample implementation and customer's need to provider their own implementation
153     * Generate user alias.
154     * Returns: Alias string (max 8 chars)
155     */
156    private static String generateAlias(String firstName, String lastName) {
157        String alias = '';
158        if (firstName != null) alias += firstName.substring(0, Math.min(firstName.length(), 4));
159        if (lastName != null) alias += lastName.substring(0, Math.min(lastName.length(), 4));
160        if (String.isBlank(alias)) alias = 'user';
161        return alias.substring(0, Math.min(alias.length(), 8));
162    }
163
164    /**
165     * Perform auto-login after successful registration and update result object
166     */
167    private static void performAutoLogin(String email, String password, String startUrl, RegistrationResult result) {
168        try {
169            PageReference loginResult = Site.login(email, password, startUrl);
170            if (loginResult != null) {
171                result.redirectUrl = loginResult.getUrl();
172                result.success = true;
173                result.message = 'Registration and login successful!';
174                System.debug(LOG_PREFIX + ' Auto-login successful, redirect URL: ' + result.redirectUrl);
175            } else {
176                result.success = true;
177                result.redirectUrl = null;
178                result.message = 'Registration successful but auto-login failed. Please login manually.';
179                System.debug(LOG_PREFIX + ' Auto-login failed - Site.login returned null');
180            }
181        } catch (Exception loginEx) {
182            result.success = true;
183            result.redirectUrl = null;
184            result.message = 'Registration successful but auto-login failed. Please login manually.';
185            System.debug(LOG_PREFIX + ' Auto-login exception: ' + loginEx.getMessage());
186        }
187    }
188
189    /**
190     * Create a business account to be associated with registering user
191     * Returns: Business account ID
192     */
193    private static String createBusinessAccount(String firstName, String lastName) {
194        Account account = new Account();
195        account.Name = firstName + ' ' + lastName;
196
197        String recordTypeId = getDefaultAccountRecordTypeId();
198        if (String.isNotBlank(recordTypeId)) {
199            account.RecordTypeId = recordTypeId;
200        }
201
202        ID siteAdminId = getSiteAdminId();
203        if (siteAdminId != null) {
204            account.OwnerId = siteAdminId;
205        }
206
207        insert account;
208        System.debug(LOG_PREFIX + ' Created business account: ' + account.Id);
209        return account.Id;
210    }
211
212    /**
213     * Get default account record type ID from Commerce configuration
214     * Returns: Account record type ID (15 chars), or null
215     */
216    private static String getDefaultAccountRecordTypeId() {
217        String networkId = Network.getNetworkId();
218
219        List<CommerceConfigRelatedRecord> configs = [
220            SELECT ConfigReferenceId
221            FROM CommerceConfigRelatedRecord
222            WHERE ContextId = :networkId
223            AND ConfigUseCase = 'SelfRegistration'
224            AND ConfigKey = 'AccountRecordType'
225            LIMIT 1
226        ];
227
228        if (!configs.isEmpty() && String.isNotBlank(configs[0].ConfigReferenceId)) {
229            String recordTypeId = configs[0].ConfigReferenceId;
230            if (recordTypeId.length() == 18) {
231                recordTypeId = recordTypeId.substring(0, 15);
232            }
233            System.debug(LOG_PREFIX + ' Found account record type ID: ' + recordTypeId);
234            return recordTypeId;
235        }
236
237        return null;
238    }
239
240    /**
241     * Get site admin ID
242     * Returns: Site admin user ID as ID, or null
243     */
244    private static ID getSiteAdminId() {
245        return Site.getAdminId();
246    }
247
248    /**
249     * Create BuyerAccount for Commerce integration
250     */
251    private static void createBuyerAccount(String accountId) {
252        BuyerAccount buyerAccount = new BuyerAccount(
253            BuyerId = accountId,
254            Name = 'Buyer-' + accountId,
255            IsActive = true
256        );
257        insert buyerAccount;
258        System.debug(LOG_PREFIX + ' Created BuyerAccount: ' + buyerAccount.Id);
259    }
260
261    /**
262     * Add buyer to all default buyer groups
263     */
264    private static void addBuyerToDefaultGroups(String accountId, List<String> defaultBuyerGroupIds) {
265        List<BuyerGroupMember> members = new List<BuyerGroupMember>();
266        for (String buyerGroupId : defaultBuyerGroupIds) {
267            if (String.isNotBlank(buyerGroupId)) {
268                members.add(new BuyerGroupMember(
269                    BuyerId = accountId,
270                    BuyerGroupId = buyerGroupId
271                ));
272            }
273        }
274
275        if (!members.isEmpty()) {
276            insert members;
277            System.debug(LOG_PREFIX + ' Added account to ' + members.size() + ' default BuyerGroups');
278        }
279    }
280
281    /**
282     * Async wrapper to assign permission set (avoids Mixed DML)
283     */
284    @future
285    private static void assignPermissionSetToUserAsync(String userId, String permissionSetGroupId) {
286        assignPermissionSetToUser(userId, permissionSetGroupId);
287    }
288
289    /**
290     * Assign permission set group to user
291     */
292    private static void assignPermissionSetToUser(String userId, String permissionSetGroupId) {
293        PermissionSetAssignment psa = new PermissionSetAssignment(
294            AssigneeId = userId,
295            PermissionSetGroupId = permissionSetGroupId
296        );
297        insert psa;
298        System.debug(LOG_PREFIX + ' Assigned permission set group to user: ' + psa.Id);
299    }
300
301    /**
302     * Get permission set group ID from NetworkSelfRegistration configuration
303     * Returns: Permission set group ID
304     */
305    private static String getPermissionSetGroupIdFromNetwork() {
306        String networkId = Network.getNetworkId();
307        if (String.isBlank(networkId)) {
308            System.debug(LOG_PREFIX + ' No network ID found');
309            throw new RegistrationException('No network ID found');
310        }
311
312        List<NetworkSelfRegistration> selfRegConfigs = [
313            SELECT PermissionSetGroupId
314            FROM NetworkSelfRegistration
315            WHERE NetworkId = :networkId
316            LIMIT 1
317        ];
318
319        if (selfRegConfigs.isEmpty() || String.isBlank(selfRegConfigs[0].PermissionSetGroupId)) {
320            System.debug(LOG_PREFIX + ' No permission set group configured in NetworkSelfRegistration');
321            throw new RegistrationException('No permission set group configured in NetworkSelfRegistration');
322        }
323
324        String permSetGroupId = selfRegConfigs[0].PermissionSetGroupId;
325        System.debug(LOG_PREFIX + ' Found permission set group ID from NetworkSelfRegistration: ' + permSetGroupId);
326        return permSetGroupId;
327    }
328
329    /**
330     * Get default buyer group IDs from CommerceConfigRelatedRecord
331     * This follows Salesforce's official implementation pattern
332     * Returns: List of BuyerGroup IDs configured for self-registration
333     */
334    private static List<String> getDefaultBuyerGroupsFromConfig() {
335        String networkId = Network.getNetworkId();
336        List<String> buyerGroupIds = new List<String>();
337
338        try {
339            List<CommerceConfigRelatedRecord> configs = [
340                SELECT ConfigReferenceId
341                FROM CommerceConfigRelatedRecord
342                WHERE ContextId = :networkId
343                AND ConfigUseCase = 'SelfRegistration'
344                AND ConfigKey = 'DefaultBuyerGroup'
345            ];
346
347            for (CommerceConfigRelatedRecord config : configs) {
348                if (String.isNotBlank(config.ConfigReferenceId)) {
349                    buyerGroupIds.add(config.ConfigReferenceId);
350                }
351            }
352
353            System.debug(LOG_PREFIX + ' Found ' + buyerGroupIds.size() + ' default buyer groups: ' + buyerGroupIds);
354        } catch (Exception e) {
355            System.debug(LOG_PREFIX + ' Error retrieving buyer groups: ' + e.getMessage());
356            throw new RegistrationException('Error retrieving buyer groups');
357        }
358
359        return buyerGroupIds;
360    }
361}

See Also