From 2835e4f2f087b6a727bda1590b6aaf18a3a4c071 Mon Sep 17 00:00:00 2001 From: Marcelo Soares Date: Tue, 18 Aug 2026 11:34:41 -0300 Subject: [PATCH 1/3] docs: Document account merging in the auth module --- .../04-working-with-users.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/06-concepts/04-authentication/04-working-with-users.md b/docs/06-concepts/04-authentication/04-working-with-users.md index 37ba31ee..6dfe0049 100644 --- a/docs/06-concepts/04-authentication/04-working-with-users.md +++ b/docs/06-concepts/04-authentication/04-working-with-users.md @@ -260,8 +260,57 @@ final additionalInfo = await MyDomainData.db.findFirstRow( ); ``` +## Merging accounts + +When a user adds a sign-in method that already belongs to a different account, the two accounts have to become one. Serverpod does not merge them on its own: you decide when to offer the merge, and run it once the user accepts. + +A merge moves the identity provider records, tokens, server-side sessions, scopes, and the user profile from one `AuthUser` to the other, then deletes the account that was merged away. All of it runs in one transaction, so a failure at any step leaves both accounts untouched. + +### Configure the merge + +Your application's own data is the part Serverpod cannot move for you. Pass an `applicationMergeHandler` to `pod.initializeAuthServices()` that reassigns your rows from the removed user to the kept one: + +```dart +pod.initializeAuthServices( + tokenManagerBuilders: [...], + accountMergeConfig: AccountMergeConfig( + applicationMergeHandler: + ( + Session session, { + required UuidValue userToKeepId, + required UuidValue userToRemoveId, + required Transaction transaction, + }) async { + await MyDomainData.db.updateWhere( + session, + where: (t) => t.authUserId.equals(userToRemoveId), + columnValues: (t) => [t.authUserId(userToKeepId)], + transaction: transaction, + ); + }, + ), +); +``` + +Without a handler, merging throws. That is the default for applications that never merge accounts. + +To reorder the built-in steps or replace one of them, use `AccountMergeConfig.custom` and pass the full list of `mergeHooks` yourself. + +### Merge two users + +```dart +await AuthServices.instance.accountMerger.merge( + session, + userToKeepId: userToKeepId, + userToRemoveId: userToRemoveId, +); +``` + +Both users must exist and be different from each other, otherwise the call throws. + ## Related - [The basics](./basics): authentication state, scopes, and endpoint access control. - [Profile photos](./profile-photos): upload, display, and default profile images. - [Setup](./setup): configure the authentication services these callbacks hook into. +- [Creating an OAuth2-based identity provider](./providers/custom-providers/oauth2-utility/creating-an-oauth2-based-identity-provider): implement `mergeAuthUsers` so a custom provider takes part in a merge. From 2107f0c890db2325e84afffa6fadb57e0446dceb Mon Sep 17 00:00:00 2001 From: Marcelo Soares Date: Thu, 20 Aug 2026 22:29:11 -0300 Subject: [PATCH 2/3] fix: Better explain the merge as a chain of hooks --- .../04-authentication/04-working-with-users.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/06-concepts/04-authentication/04-working-with-users.md b/docs/06-concepts/04-authentication/04-working-with-users.md index 6dfe0049..47a011ca 100644 --- a/docs/06-concepts/04-authentication/04-working-with-users.md +++ b/docs/06-concepts/04-authentication/04-working-with-users.md @@ -264,11 +264,18 @@ final additionalInfo = await MyDomainData.db.findFirstRow( When a user adds a sign-in method that already belongs to a different account, the two accounts have to become one. Serverpod does not merge them on its own: you decide when to offer the merge, and run it once the user accepts. -A merge moves the identity provider records, tokens, server-side sessions, scopes, and the user profile from one `AuthUser` to the other, then deletes the account that was merged away. All of it runs in one transaction, so a failure at any step leaves both accounts untouched. +A merge runs as an ordered list of hooks (declared in `mergeHooks`). All hooks run inside one transaction, so a failure at any step leaves both accounts untouched. By default, `AccountMergeConfig` fills the list with these hooks: + +| Hook | What it does | +| --- | --- | +| `defaultIdpMergeHandler` | Calls `mergeAuthUsers` on every initialized identity provider (e.g. Email, Google, Apple, etc.). | +| `defaultCoreDataMergeHandler` | Merges the scopes of the `AuthUser` records, and moves the tokens, server-side sessions, and user profile. | +| `applicationMergeHandler` | Moves your application's own data. **This is the one you write.** | +| `defaultMergeCleanupHandler` | Deletes the `AuthUser` that was merged away. | ### Configure the merge -Your application's own data is the part Serverpod cannot move for you. Pass an `applicationMergeHandler` to `pod.initializeAuthServices()` that reassigns your rows from the removed user to the kept one: +Your application's own data is the part Serverpod cannot move for you. Pass the `applicationMergeHandler` to `pod.initializeAuthServices()` that reassigns your rows from the removed user to the kept one: ```dart pod.initializeAuthServices( @@ -292,9 +299,11 @@ pod.initializeAuthServices( ); ``` +Your handler only needs to move data over. The `defaultMergeCleanupHandler` runs after it and deletes the removed `AuthUser`, which cascades to all rows that reference it with `onDelete=Cascade`. + Without a handler, merging throws. That is the default for applications that never merge accounts. -To reorder the built-in steps or replace one of them, use `AccountMergeConfig.custom` and pass the full list of `mergeHooks` yourself. +To reorder the built-in hooks or replace one of them, use `AccountMergeConfig.custom` and pass the full list yourself. ### Merge two users From 77058202e118e1dfffcc6380e5c6a73aaecfb9d7 Mon Sep 17 00:00:00 2001 From: Marcelo Soares Date: Fri, 21 Aug 2026 14:32:28 -0300 Subject: [PATCH 3/3] docs: Address review on the core data merge handler --- docs/06-concepts/04-authentication/04-working-with-users.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/06-concepts/04-authentication/04-working-with-users.md b/docs/06-concepts/04-authentication/04-working-with-users.md index 47a011ca..a994f6ba 100644 --- a/docs/06-concepts/04-authentication/04-working-with-users.md +++ b/docs/06-concepts/04-authentication/04-working-with-users.md @@ -269,7 +269,7 @@ A merge runs as an ordered list of hooks (declared in `mergeHooks`). All hooks r | Hook | What it does | | --- | --- | | `defaultIdpMergeHandler` | Calls `mergeAuthUsers` on every initialized identity provider (e.g. Email, Google, Apple, etc.). | -| `defaultCoreDataMergeHandler` | Merges the scopes of the `AuthUser` records, and moves the tokens, server-side sessions, and user profile. | +| `defaultCoreDataMergeHandler` | Merges the scopes and the blocked flag of the `AuthUser` records, moves the refresh tokens and server-side sessions, and merges the user profiles. A user blocked on either account stays blocked after the merge. | | `applicationMergeHandler` | Moves your application's own data. **This is the one you write.** | | `defaultMergeCleanupHandler` | Deletes the `AuthUser` that was merged away. |