Basilic
Architecture

Account linking

Change email vs link email, OAuth provider linking, provider trust, and guardrails.

Overview

Account linking lets users attach additional sign-in methods (email, OAuth providers, wallets, passkeys) to an existing account. Two primary patterns exist: change email (replace primary email) and link email (set primary email when none exists).

CapabilityChange emailLink email
EffectReplaces users.emailSets users.email when it is currently null (e.g. Web3-only account)
FlowRequest → verify code/link → updateRequest → verify link → set email
Use caseUser changes email addressUser adds email sign-in to a wallet/OAuth-only account
Already has emailAllowed (change)409 EMAIL_ALREADY_SET (use change email)
Email owned by another user409 EMAIL_ALREADY_IN_USE409 EMAIL_ALREADY_IN_USE

Change email uses change_email verification type; link email uses link_email. Change email uses 6-digit + link UX with 15-minute TTL. Link email uses a link token (no 6-digit code).

Verify modes (change email)

Verification accepts exactly one of:

  • { token, email } — code entry flow; email used as identifier
  • { token, verificationId } — link-click flow; verificationId from callback URL

Both/neither returns 400 INVALID_PAYLOAD.

OAuth provider linking

Logged-in users can link additional OAuth providers from Profile (Settings). The backend:

  • Uses oauth_link_state (distinct from oauth_state for sign-in)
  • Binds the link to meta.userId so the exchange attaches to the correct account
  • Stores meta.redirectUri for Google (from redirect_uri query) when multiple callback URLs are configured (web + mobile)
  • Returns 409 PROVIDER_ALREADY_LINKED when the provider is linked to another user
  • Returns redirectTo so the client can return the user to the settings page after success

GET /auth/oauth/{provider}/link-authorize-url requires Bearer auth. When OAuth env is unset, returns 503 OAUTH_NOT_CONFIGURED. Live IdP exchange is not covered by unit tests — see Testing.

For Google, when using multiple callback URLs (e.g. web + mobile app), the client passes redirect_uri in the link-authorize-url request. It must be in the allowlist (OAUTH_GOOGLE_CALLBACK_URLS). See Authentication for full Google OAuth setup.

Provider trust

OAuth providers are trusted for identity on first link. Subsequent links require re-authorization; existing tokens are replaced on each successful link.

Guardrail: last sign-in method

Users must keep at least one sign-in method. Sign-in methods: users.email, OAuth account, wallet_identities, passkey_credentials. TOTP is 2FA only — unlinking TOTP is always allowed.

Unlinking wallet, OAuth, or passkey fails with 400 LAST_SIGN_IN_METHOD if it would leave no way to sign in.

On this page