This error occurs when attempting to link an OAuth identity that is already associated with another user account in Supabase Auth, commonly when converting anonymous users to verified accounts.
The "identity_already_exists" error is thrown by Supabase Auth when you call `linkIdentity()` to associate an OAuth provider identity (like Google, GitHub, or Facebook) with a user account, but that specific identity is already linked to a different user in your database. This error is part of Supabase's identity linking system, which allows you to connect multiple authentication methods to a single user account. However, each unique identity (defined by the provider and the provider's user ID) can only be associated with one user account at a time. When Supabase detects that you're trying to link an identity that's already in use, it prevents the operation to maintain data integrity. A common scenario is when a user signs in with an OAuth provider, then your app automatically creates an anonymous session (perhaps after a reinstall), and you later attempt to link the same OAuth identity to the anonymous account. Since that OAuth identity is already tied to the original verified account, the link operation fails with this error.
After calling linkIdentity(), Supabase redirects to your configured redirect URL with error information. Check the URL parameters for the error:
// Listen for the deep link or redirect
useEffect(() => {
const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
console.log('Auth event:', event);
});
// For mobile apps, listen to deep link
const url = Linking.getInitialURL();
url.then((deepLink) => {
if (deepLink?.includes('error=identity_already_exists')) {
console.log('Identity already linked to another user');
// Handle error appropriately
}
});
}, []);For web apps, check the URL after redirect:
// Parse URL parameters after redirect
const params = new URLSearchParams(window.location.search);
const error = params.get('error');
const errorDescription = params.get('error_description');
if (error === 'identity_already_exists') {
console.log('Error:', errorDescription);
// Show user-friendly message
}The linkIdentity() method requires the "Enable Manual Linking" option to be enabled in your Supabase project settings:
1. Go to your Supabase Dashboard
2. Navigate to Authentication → Settings
3. Scroll to "Auth Providers" section
4. Ensure "Enable Manual Linking" is toggled ON
If this setting is disabled, identity linking operations will fail regardless of whether the identity already exists.
When the identity is already linked to another account, give users the option to sign in with that existing account instead:
async function handleIdentityLink(provider: 'google' | 'github' | 'facebook') {
try {
const { data, error } = await supabase.auth.linkIdentity({
provider: provider,
});
if (error) {
throw error;
}
// Success - identity linked
console.log('Identity linked successfully');
} catch (error: any) {
// Error is typically returned via redirect URL, not thrown
// Check redirect URL in your auth callback handler
}
}
// In your auth callback handler (after redirect)
function handleAuthCallback() {
const params = new URLSearchParams(window.location.search);
const error = params.get('error');
if (error === 'identity_already_exists') {
// Show user a message with options
showErrorDialog({
title: 'Account Already Exists',
message: 'This account is already linked to another user. Would you like to sign in with that account instead?',
actions: [
{
label: 'Sign In',
onClick: () => signInWithProvider(provider)
},
{
label: 'Use Different Account',
onClick: () => resetAndRetry()
}
]
});
}
}To prevent the error proactively, check if a user already has an identity linked before attempting to add a new one:
async function checkExistingIdentities() {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
throw new Error('No user signed in');
}
// Get all identities for current user
const identities = user.identities || [];
console.log('Existing identities:', identities.map(i => ({
provider: i.provider,
id: i.id,
created_at: i.created_at
})));
// Check if specific provider is already linked
const hasGoogleIdentity = identities.some(i => i.provider === 'google');
if (hasGoogleIdentity) {
console.log('Google identity already linked');
return false; // Don't attempt to link
}
return true; // Safe to link
}
// Use before calling linkIdentity
async function safeLinkIdentity(provider: string) {
const canLink = await checkExistingIdentities();
if (!canLink) {
console.log('Identity already exists for this provider');
return;
}
// Proceed with linking
const { error } = await supabase.auth.linkIdentity({ provider });
// Handle result
}If you're converting anonymous users to verified accounts, implement a strategy to handle cases where the OAuth identity already exists:
async function convertAnonymousToVerified(provider: 'google' | 'github') {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
throw new Error('No user session');
}
// Check if user is anonymous
const isAnonymous = user.is_anonymous ||
(user.identities && user.identities.length === 0);
if (!isAnonymous) {
console.log('User is already verified');
return;
}
// Attempt to link identity
const { data, error } = await supabase.auth.linkIdentity({ provider });
// Since error comes via redirect, set up listener
// In your redirect handler:
const handleRedirect = () => {
const params = new URLSearchParams(window.location.search);
if (params.get('error') === 'identity_already_exists') {
// Identity exists on another account
// Option 1: Sign user out and redirect to sign in
await supabase.auth.signOut();
await supabase.auth.signInWithOAuth({ provider });
// Option 2: Transfer anonymous user's data to existing account
// (requires custom server-side logic)
}
};
}Error Handling Quirk: Unlike most Supabase Auth errors, identity_already_exists is not thrown as a direct exception when calling linkIdentity(). Instead, Supabase redirects your application to the configured redirect URL with the error information embedded in the URL query parameters. This means you must implement redirect URL parsing and deep link handling to properly catch and display this error to users.
Database-Level Constraints: The uniqueness of identities is enforced at the database level in Supabase's auth.identities table. Each combination of provider and provider_id must be unique across all users. This constraint prevents identity hijacking and ensures data integrity.
Merging Accounts: If you need to merge two accounts (for example, transferring data from an anonymous account to an existing verified account), you'll need to implement custom server-side logic. Supabase doesn't provide a built-in account merging feature. You would typically:
1. Query both user accounts
2. Transfer data (user-generated content, settings, etc.) from the anonymous account to the verified account
3. Delete the anonymous account
4. Sign the user in to the verified account
Rate Limiting: Be aware that OAuth providers like Google and GitHub have rate limits on authentication requests. If users repeatedly attempt to link identities and encounter errors, you may hit these limits. Implement exponential backoff or cooldown periods for retry attempts.
Testing Identity Linking: In development, you may encounter this error frequently when testing with the same OAuth account. To reset, you can manually delete identity records from your Supabase dashboard (Authentication → Users → select user → Identities tab) or use the Supabase Management API to programmatically remove identities during testing.
email_conflict_identity_not_deletable: Cannot delete identity because of email conflict
How to fix "Cannot delete identity because of email conflict" in Supabase
mfa_challenge_expired: MFA challenge has expired
How to fix "mfa_challenge_expired: MFA challenge has expired" in Supabase
conflict: Database conflict, usually related to concurrent requests
How to fix "database conflict usually related to concurrent requests" in Supabase
phone_exists: Phone number already exists
How to fix "phone_exists" in Supabase
StorageApiError: resource_already_exists
StorageApiError: Resource already exists