Account Merge
It is common for one person to accumulate several logi accounts over time. You might have first signed in with Apple on one device and Google on another, or browsed anonymously first and then created another anonymous account on a different device before linking SSO. Sometimes it is simply because you use two email addresses.
logi offers three paths to combine these split accounts into one. The domain data you have built up at an RP (a service) — records, notes, payment history, and so on — is preserved as-is. Whenever a merge happens, it is recorded in the audit log and the identity_links table, so you or an operator can always verify later which accounts were merged.
What happens once a merge completes
When a merge succeeds, the two accounts are consolidated into a single primary account (survivor), and the absorbed side (linked_user_id) no longer receives new tokens. From then on, every RP sees only the primary account's canonical identifier (survivor_canonical_sub).
Internally, one row is added to the identity_links table:
primary_user_id (= primary account, survivor)
linked_user_id (= absorbed account)
merged_via ("t1_device_link" | "t2_email_match" | "t3_otp")
idempotency_key (retry-safe)The absorbed account's row itself is not deleted — the merge trace remains intact, and the RP's foreign key (logi_identity_links) is updated through the user.merged webhook the RP receives.
The design guarantees:
- Domain data is preserved — the tournament records, notes, payment history, and other data an RP holds survive intact through canonical resolution. (However, the absorbed account's session tokens, app keychain, and browser cookies are invalidated immediately for security reasons. For the full scope, see What gets disconnected.)
- Safe to retry — calling twice with the same
idempotency_keyproduces the same result, and a concurrency conflict returns:already_processed. - No chains or cycles — chained merges such as
A→BandB→Care blocked by a DB-level trigger. The primary account is always resolved within a single hop.
The three triggers
T1 — Automatic merge on the same device (device-link)
When you successfully complete both Apple SSO and Google SSO on the same device, logi has enough grounds to conclude that the two accounts belong to the same person (owning the device serves as proof of identity). In this case the accounts are merged with merged_via=t1_device_link without any additional verification.
From your perspective, it ends with a single "your two accounts have been merged" notice right after the second SSO.
For the detailed flow, see T1 / T2 / T3 trigger sequence.
T2 — Signing in with a different provider that uses the same email
When someone who first signed up with Apple later signs in with Google SSO for the first time (or vice versa), and the emails returned by the two providers match and are both verified, logi interprets this as the same person's second SSO.
Internally:
- On the new SSO callback,
Auth::ProviderMatcherlooks up the existing account byemail_address. - If the existing account already holds another provider's
*_sub, logi creates a temporary account (a ghost row holding only the new provider sub) and then immediately absorbs it into the primary account viaMergeService. - The sub/email that the temporary account held is moved to the primary account within the same transaction.
From your perspective, it looks like an ordinary "sign-in success." Automatic merge does not happen if either email is unverified or if the two emails differ.
T3 — Merging two accounts yourself
This path is for when you want to explicitly combine two accounts that you started with different SSO providers on different devices. On logi's /me/merge consent screen:
- Step A — Enter the email of the account you want to merge into the current one. logi sends a 6-digit OTP to that email (the verified contact_email).
- Step B — Enter the OTP you received. Once verified, the merge runs immediately, and the account you are currently signed in to becomes the primary account.
- Step C — Merge-completion notice.
T3 verifies your identity on both sides (dual proof-of-possession). The current session is confirmed with a cookie or PAK, and the side being absorbed is confirmed with the OTP that arrived at the verified email. If either one is missing, the flow does not advance to the next step.
When both sessions are active on the same device, you can use session_token PoP instead of an OTP.
For security details, see Merge Idempotency and Threat Model.
What gets disconnected
For security, all credentials held by the absorbed account are invalidated immediately within the merge transaction (nine items):
- Access Token / Refresh Token / Authorization Code
- Personal API Key (PAK)
- OAuth Grant (per RP)
- Active sessions (browser cookies)
- Device credentials (app keychain)
- WebAuthn credentials (passkeys)
- TOTP / backup codes
- In-progress password reset / email change tokens
- In-progress merge OTP tokens
The invalidation above applies only to the absorbed side. The primary account's credentials remain intact, so you do not need to log out or sign in again right after the merge.
How it looks from the RP (service) side
📋 Production canonicalization policy
Production environments (for example, the Easy Bracket production RP) have run with ENFORCE_CANONICAL_RESOLUTION=true since 2026-05-11. For a merged user, the RP always receives a sub claim canonicalized to canonical_sub, and a token issued with a stale merged_sub is rejected immediately on validation. Be sure to route your RP-side logic through the canonical resolver.
The RP receives a user.merged webhook. For the payload structure and the idempotency contract, see Event Delivery. On receiving the webhook, the RP inserts one logi_identity_links(primary_user_id, linked_user_id) row into its own DB, and routes every subsequent user lookup through the canonical resolver.
The access tokens issued by logi are revoked automatically. Note that a session token the RP issued on its own may remain valid for a brief window until the next token rotation or polling cycle comes around — this short gap is explicitly allowed behavior, and how to handle it is laid out in the RP Migration Guide.
Can it be undone?
The current v3 does not provide an unmerge feature. The merge trace remains in the audit log and identity_links, so it is traceable, but there is no separate API for a user or operator to "split" accounts. This is intended by design for data-consistency and security reasons — the detailed rationale and the operational procedure when forensic recovery is needed are covered in the Rollback Policy.