Provide the user with an option to delete their own account

Introduction:
In my website, I wanted to provide users with an option to delete their own account (which is, in my opinion, an essential feature). As it didn’t exist by default, I searched the APIs to create it. Just as a background, I manage my website’s users in a “UserProfiles” collection.

How I did it:
The code is as follows. I’m skipping try-catch blocks, error checks and logs so it becomes shorter and straight to the point.

import { currentUser } from 'wix-users-backend';
import { Permissions, webMethod } from 'wix-web-module';
import { secrets } from 'wix-secrets-backend.v2';
import * as wixAuth from 'wix-auth';
import wixData from 'wix-data';
import { fetch } from 'wix-fetch';
import { ok } from 'wix-http-functions';
import { contacts } from 'wix-crm-backend';

export const deleteUserAccount = webMethod(Permissions.Anyone, async () => {
  const user = currentUser;
  const userId = user.id;

  // Get admin access token and site ID
  let adminToken = await getWixSecret("MAIN_KEY");
  let siteId = await getWixSecret("SITE_ID");

  // Delete user profile from UserProfiles collection first
  const userProfile = await wixData.query("UserProfiles")
    .eq("userId", userId)
    .find();
    
  if (userProfile.items.length > 0) {
    await wixData.remove("UserProfiles", userProfile.items[0]._id);
  }

  // Get member email to find contact ID via contacts collection
  let contactId = null;

  const memberData = await wixData.get("Members/PrivateMembersData", userId);
      
  // Get member email
  let memberEmail = memberData.loginEmail;
      
  // Query contacts collection by email to find contact ID
  if (memberEmail) {
    const contactsResponse = await fetch(`https://www.wixapis.com/contacts/v4/contacts?query=${encodeURIComponent(memberEmail)}`, {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${adminToken}`,
        'wix-site-id': siteId,
        'Content-Type': 'application/json'
      }
    });
          
    if (contactsResponse.ok) {
      const contactsData = await contactsResponse.json();
            
      if (contactsData.contacts && contactsData.contacts.length > 0) {
        // Find contact with matching email
        const matchingContact = contactsData.contacts.find(contact => 
          contact.primaryInfo?.email === memberEmail || 
          contact.emails?.some(email => email.email === memberEmail)
        );
              
        if (matchingContact) {
          contactId = matchingContact.id || matchingContact._id;
        }
      }
    }
  }

  // Delete the member first using Wix Members API (required before contact deletion)
  const memberResponse = await fetch(`https://www.wixapis.com/members/v1/members/${userId}`, {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${adminToken}`,
      'wix-site-id': siteId,
      'Content-Type': 'application/json'
    }
  });

  // Delete the contact after member deletion (as required by API)
  if (contactId) {
    // Wait for member deletion to propagate
    await new Promise(resolve => setTimeout(resolve, 5000));
    const contactResponse = await fetch(`https://www.wixapis.com/contacts/v4/contacts/${contactId}`, {
      method: 'DELETE',
      headers: {
        'Authorization': `Bearer ${adminToken}`,
        'wix-site-id': siteId,
        'Content-Type': 'application/json'
      }
    });
  }
    
  return ok({ 
    message: 'Account deleted successfully. You have been logged out.',
    userId: userId
  });
});

// Function used to get secrets from the Wix dashboard
export const getWixSecret = webMethod(Permissions.Anyone, async (key) => { 
    const elevateSecret = wixAuth.elevate(secrets.getSecretValue);
    if (key == "MAIN_KEY") {
        return (await elevateSecret('MAIN_KEY')).value;
    }
    else if (key == "SITE_ID") {
        return (await elevateSecret('SITE_ID')).value;
    }
    else {
        return null;
    }
});

Tools, Documentation and APIs:
These two references were particuarly important in the process of creating this feature:

Notes:
Sorry if there are some imports missing or any flaws in the code. I tried to display it as simply as possible. There might be better ways to do this, but at least this worked for me, so it would be cool if it would work for you as well :slight_smile:

Cheers!