Linking identities

How Supabase Auth links several sign-in methods to one user on SupaHost: automatic linking by verified email, manual linking with linkIdentity, listing and unlinking identities.

One person can sign in to your app in several ways: a password today, Google next week, GitHub from their work laptop. Each way is an identity, stored as a row in auth.identities, and all of them can point at the same row in auth.users. Linking keeps them as one user, with one user id, so their data follows them whichever button they press.

There are two kinds of linking.

Automatic linking

When someone signs in with a provider and the provider vouches for an email address that already belongs to a user, Auth adds the new identity to that existing user instead of creating a second one.

For example, Jo signs up with [email protected] and a password, and confirms the address. Later Jo clicks "Continue with Google" with the same Gmail address. Auth sees a verified email that matches, links the Google identity to Jo's existing user, and Jo lands in the same account with the same data.

This only happens with verified emails. If either side's email isn't verified (an unconfirmed sign-up, or a provider that doesn't confirm emails), Auth doesn't link, because doing so would let anyone who registers an address they don't own take over the real owner's account. That's also why turning on "Skip email confirmation" in production weakens this protection: addresses are then treated as confirmed without proof.

Automatic linking is always on. There's no setting for it.

Manual linking

Manual linking lets a signed-in user attach another provider to their account, even if its email is different (a personal Gmail on a work account, say). Turn it on in Dashboard → your project → Auth tab → Sign-in & security → Manual identity linking and click Save sign-in settings.

Then, from a settings page where the user is signed in:

const { data, error } = await supabase.auth.linkIdentity({
  provider: 'github',
  options: { redirectTo: 'https://chat.example.com/settings/accounts' },
})

This works like an OAuth sign-in: the user goes to GitHub, approves, and comes back to redirectTo (which must be in your Redirect URLs). Instead of signing in as someone new, the GitHub identity is added to the current user. The provider must be enabled in the OAuth providers card.

If manual linking is off, the call fails with the error code manual_linking_disabled. If the GitHub account is already linked to a different user, it fails with identity_already_exists.

Listing a user's identities

const { data, error } = await supabase.auth.getUserIdentities()

for (const identity of data!.identities) {
  console.log(identity.provider, identity.identity_data?.email, identity.created_at)
}

A settings page for a team chat app might show a row per provider, with "Connect" for the missing ones (calling linkIdentity) and "Disconnect" for the linked ones.

Unlinking

const { data } = await supabase.auth.getUserIdentities()
const github = data!.identities.find((i) => i.provider === 'github')

if (github) {
  const { error } = await supabase.auth.unlinkIdentity(github)
}

A user must keep at least one identity. Unlinking the last one fails with single_identity_not_deletable. Before unlinking, make sure the user has another way to sign in that they can actually use, such as a password they know.

Unlinking removes the identity, not the user, and it doesn't change the user's email. If the unlinked provider's email was the user's primary address, update it with updateUser({ email }) if needed.

Linking from an anonymous user

Anonymous users become permanent by linking. Adding an email with updateUser({ email }) doesn't need manual linking. Adding an OAuth provider with linkIdentity does, so turn on Manual identity linking if your guests should be able to "Continue with Google" to keep their data.

Where identities show up

  • user.identities on the user object, returned by getUser().
  • app_metadata.providers in the user's JWT, a list such as ["email", "google"], which you can read in policies with auth.jwt().
  • The auth.identities table, which you can query in Studio's SQL editor:
select u.email, i.provider, i.created_at
from auth.identities i
join auth.users u on u.id = i.user_id
order by u.email, i.created_at;

Linking users who already have two accounts

If someone ended up with two separate users (for example a Google sign-in before email linking applied, with an unverified email), Auth won't merge them. Merging is your job: pick the user to keep, move rows from the other user's id to it with the service_role key, then delete the extra user with the admin API. See Managing users.

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us