Identity Linking for Self-Hosted Supabase: A Setup Guide

Let users connect Google, GitHub, and email logins to one account on self-hosted Supabase. Covers automatic vs manual linking, the GoTrue flag, and gotchas.

Cover Image for Identity Linking for Self-Hosted Supabase: A Setup Guide

A user signs up with Google. Three weeks later they come back, click "Sign in with GitHub" because they forgot which button they used, and end up staring at an empty account. Same person, same email, two completely separate users in your database. It's one of the most common support tickets in any app with social login — and on self-hosted Supabase it's quietly worse, because the feature that fixes it is turned off by default and there's no UI toggle to flip.

This guide covers how identity linking actually works in self-hosted Supabase, the difference between automatic and manual linking, and the one environment variable that trips up nearly everyone. If you haven't wired up your providers yet, start with setting up OAuth providers for self-hosted Supabase first — identity linking assumes you already have at least two working login methods.

The Two Kinds of Identity Linking

Supabase Auth (the service formerly and still often called GoTrue) handles linking in two distinct ways. Knowing which one applies to your situation saves a lot of confusion.

Automatic linking happens on its own when a user signs in with a new provider that reports the same verified email as an existing account. If [email protected] first signs up with email/password and later authenticates with Google using that same address, GoTrue matches them and attaches the Google identity to the existing user. No code required.

The critical caveat: both emails must be verified. This isn't bureaucratic caution — it's the entire security model. If GoTrue linked unverified emails, an attacker could create an account with [email protected] on a provider that doesn't verify ownership, then inherit Jane's real account the moment she logs in. Verified-only linking closes that door. It also means automatic linking does nothing for providers that don't return an email at all, which is common with some OAuth and OIDC setups.

Manual linking is the explicit version. An already-logged-in user clicks "Connect GitHub" in their settings, and your app calls linkIdentity() to attach that identity — even if the GitHub email is completely different from their current one. This is what you want for "connect your accounts" settings pages, and it's the path that requires the extra configuration step below.

The Self-Hosting Gotcha: Manual Linking Is Disabled

On Supabase Cloud, you flip manual linking on in the dashboard. On a self-hosted instance there is no such switch — you set an environment variable and restart the auth container.

# In your .env file for the auth service
GOTRUE_SECURITY_MANUAL_LINKING_ENABLED=true

If you're running the standard docker-compose.yml, make sure the variable is actually passed through to the auth service:

auth:
  image: supabase/gotrue:latest
  environment:
    GOTRUE_SECURITY_MANUAL_LINKING_ENABLED: ${GOTRUE_SECURITY_MANUAL_LINKING_ENABLED}
    # ... your other GOTRUE_ vars

Then restart:

docker compose up -d auth

Until you do this, every call to linkIdentity() from your client comes back with a 422 / "Manual linking is disabled" error, and you'll burn an afternoon assuming your client code is broken. It isn't. The default for this flag is false, full stop. This is the single most common identity-linking complaint from self-hosters, and it's purely a configuration issue. If you're auditing your full config while you're in there, our complete guide to self-hosted Supabase environment variables maps out the rest of the GOTRUE_ namespace.

Implementing Manual Linking in Your App

With the flag enabled, the client API is straightforward. To start a link flow for the currently signed-in user:

const { data, error } = await supabase.auth.linkIdentity({
  provider: 'github',
})
// Redirects to GitHub, then back to your redirectTo URL.

The user completes the OAuth dance and returns with GitHub now attached to their existing account — no new user row created. Note this requires an active session; linking is an operation on a logged-in user, not a way to log in.

To show users what they've already connected (for a settings page):

const { data, error } = await supabase.auth.getUserIdentities()
// data.identities → [{ provider: 'email', ... }, { provider: 'github', ... }]

And to disconnect one:

const identity = data.identities.find((i) => i.provider === 'github')
const { error } = await supabase.auth.unlinkIdentity(identity)

Two constraints worth designing around:

  • A user must have at least two identities to unlink one. GoTrue won't let someone strip their last login method and lock themselves out. Your UI should disable the "disconnect" button when only one identity remains.
  • SAML SSO identities cannot be link targets. If you're running enterprise SSO, those users sit outside the linking model by design.

For native mobile flows where you already hold an ID token from Google or Apple, linkIdentityWithIdToken() skips the web redirect entirely — useful when you're using native sign-in sheets rather than a browser popup.

Verified Emails: The Part That Bites Production

Automatic linking lives or dies on email verification, and self-hosted instances are exactly where verification gets misconfigured. A few failure modes to check before you ship:

  • SMTP isn't actually sending. If confirmation emails never arrive, users never verify, and automatic linking silently never triggers — you just get duplicate accounts with no error anywhere. Confirm your mail setup is solid; our SMTP and email template configuration guide covers the common deliverability traps.
  • A provider that doesn't return email. Some OIDC providers omit email or mark it unverified. Those identities will never auto-link; manual linking is your only option for them.
  • Multiple unverified signups. If you've disabled email confirmation for convenience (a tempting shortcut in dev that leaks into prod), you've also disabled the safety check that makes automatic linking trustworthy. Don't.

A useful sanity query against your database — every duplicate here is a user who'll eventually file a ticket:

select email, count(*)
from auth.users
group by email
having count(*) > 1;

If that returns rows, you have accounts that should have linked and didn't. Clean-up is manual and tedious, which is the whole argument for getting the configuration right up front.

Where Supascale Fits

The frustrating part of all this isn't the concept — it's that on self-hosted Supabase the controls are scattered across environment variables, container restarts, and a docker-compose.yml you hand-edit, with no dashboard to confirm anything took effect. Toggle the wrong flag and your only feedback is a cryptic 422 in production.

Supascale manages the auth service configuration for your self-hosted projects through a UI, so flags like manual linking and your OAuth provider credentials are set, validated, and applied without hand-editing compose files or guessing whether the container picked up your change. Combined with automated backups and one-click restore, it removes most of the "did that actually save?" anxiety that makes self-hosting auth feel fragile — for a one-time price rather than a per-project monthly bill. You still own the infrastructure and the data; you just stop fighting the config surface.

Conclusion

Identity linking isn't complicated once you know the rules: automatic linking handles same-email logins for free (as long as emails are verified), and manual linking handles everything else — but only after you set GOTRUE_SECURITY_MANUAL_LINKING_ENABLED=true and restart the auth container. Get email verification right, guard against unlinking the last identity, and run the duplicate-check query before you launch. Do that, and the "two accounts, one human" support ticket disappears from your queue.

Ready to stop hand-editing compose files for every auth tweak? Start with Supascale and manage your self-hosted Supabase auth from one place.

Further Reading