threshold1

External user ID bridging

If you already have users in your own database, you don't need threshold1's internal UUID to be your source of truth. Pass your own externalUserId alongside a login or registration, and threshold1 links its own user record to it — so every method (passkey, OTP, magic link, BYOAM) resolves to the same account, keyed by the ID you already use.

How resolution actually works

Every method — passkey register/login, OTP verify, magic-link verify, BYOAM's confirm callback — funnels through one shared identity-resolution function on the backend. It checks, in strict order:

  1. externalUserId match — if you passed one and it matches an existing user, that's the account. Full stop; this always wins first.
  2. Email match — only among users that don't already have a different externalUserId attached. If found, and you did pass an externalUserId this time, it gets attached now (upgrading that user from threshold1-managed to externally-bridged).
  3. Create a new user — if nothing matched.

The identity rule this enforces: externalUserId is the primary identifier once set; email is secondary, used for linking and for OTP/magic-link delivery, never for re-matching a user who's already bridged to a different ID. A user can never be silently reassigned to a different externalUserId than the one they already have.

// First login: no existing threshold1 user yet — creates one, bridges it
// to your own ID immediately.
await auth.register({ email: "user@example.com", externalUserId: "your_user_123" });

// Any later call with this externalUserId resolves to the exact same
// threshold1 user, regardless of which method is used to get there.
const user = await auth.login("user@example.com");
// user.externalUserId === "your_user_123"

Email is preserved, never overwritten, on a match

When a login resolves via externalUserId match, the email you happen to pass along with it is never written over the user's existing stored email — even if it's different from what's on file. The stored value wins; the incoming one is only checked for consistency and logged if it doesn't match, never applied.

This matters because the email you pass isn't necessarily fresh or authoritative just because it was proven for this login — a mismatch is far more likely to mean the wrong externalUserId got attached somewhere in your own system than that the user's email genuinely changed. threshold1 doesn't guess which explanation is right; it just refuses to silently overwrite a real, existing value based on one login's input.

// First call for this externalUserId — creates the user with this email.
await auth.register({ email: "user@example.com", externalUserId: "your_user_123" });

// Later, a different email arrives for the SAME externalUserId.
await auth.login({ email: "new-address@example.com", externalUserId: "your_user_123" });
// The stored email is still "user@example.com" — untouched.
// getUser().email reflects the real, unchanged value.

If you genuinely need to update a user's email on file, that's a deliberate action you take yourself (e.g. through your own backend, directly), not a side effect of them logging in with a different address attached to the same ID.

addPasskey({ externalUserId, email }) follows the identical rule for both fields: externalUserId is only attached if the user doesn't already have a different one, and email only fills a gap — it's applied if the user has no email on file yet, and never overwrites one that's already there.

What's next

  • Environments covers how this resolution is scoped per test/live environment — the same email can be two different users across the two.
  • Passkey-first & smart fallback covers the cascade that most commonly calls into this resolution logic.
  • BYOAM covers the confirm-callback path, which also resolves through this same identity bridge.