feat: email confirmation for org deletion & ownership transfer, plus opt-in confirmationMode
Is this suited for github?
- Yes, this is suited for github
Is your feature request related to a problem? Please describe.
The user.deleteUser flow supports an optional email confirmation step
(sendDeleteAccountVerification): the user requests deletion, gets an email
with a confirmation link, and the account is only deleted after they click
it. This protects against accidental or malicious one-click account deletion
(e.g. XSRF-ish misuse, a compromised session, a stray API call).
The organization plugin has no equivalent for its two most destructive member-facing actions:
organization/deletedeletes the organization immediately once the caller passes theorganization:deletepermission check — no confirmation step.- There is no "transfer ownership" operation at all. Reassigning the
creatorRole(owner) today means callingorganization/update-member-roledirectly, which takes effect immediately with no verification that the current owner actually intended to give up control.
Both are effectively irreversible from the acting member's perspective (data
loss for deletion, loss of control for ownership transfer), so they deserve
the same opt-in confirmation pattern deleteUser already has.
Describe the solution you'd like
Mirror the existing user.deleteUser contract for these two organization
actions, as two independent, opt-in features under OrganizationOptions:
1. Organization deletion confirmation
organization({
organizationDeletion: {
sendDeleteOrganizationVerification: async ({ organization, user, url, token }, request) => {
// send email
},
deleteTokenExpiresIn: 60 * 60 * 24, // default 1 day
},
})organization/deletegains an optionaltokenfield in its body, same asdeleteUser. WithoutsendDeleteOrganizationVerificationconfigured, behavior is unchanged (immediate deletion) — fully backward compatible.- With it configured, the first call creates a single-use verification token
scoped to
(organizationId, requesting user), emails a callback URL, and returns{ success: true, message: "Verification email sent" }instead of deleting. - New
GET /organization/delete/callback?token=...endpoint (mirroring/delete-user/callback) re-validates the session and permission, consumes the token atomically, then runs the existing deletion path (includingbeforeDeleteOrganization/afterDeleteOrganizationhooks).
2. Ownership transfer confirmation
A new dedicated endpoint rather than overloading update-member-role (which
is a generic, synchronous, multi-role endpoint and shouldn't grow an async
confirmation branch):
organization({
ownershipTransfer: {
sendTransferOwnershipVerification: async ({ organization, currentOwner, newOwner, url, token }, request) => {
// send email
},
transferTokenExpiresIn: 60 * 60 * 24,
},
})POST /organization/transfer-ownership— body:organizationId,newOwnerMemberId. Callable only by a current owner (or someone withorganization:updateon the owner role, consistent with today's last-owner rules inupdate-member-role).- Without
sendTransferOwnershipVerificationconfigured: performs the same atomic demote-current/promote-target role swap that callingupdate-member-rolewith the creator role does today. - With it configured: emails the current owner (not the target — they
didn't initiate anything) a confirmation link; only on click does the
atomic role swap happen. Token is single-use, scoped to
(organizationId, current owner, target member)so it can't be replayed against a different target after org membership changes. - Reuses the existing "can't leave an org with zero owners" guard from
update-member-role/remove-member/leave-organization.
Both features:
- Are fully opt-in (no config change = current behavior, no breaking change).
- Get
beforeDelete/afterDelete-style hooks where useful, consistent with existingorganizationHooks. - Ship with unit tests under
packages/better-auth/src/plugins/organization/routes/*.test.tsand docs updates indocs/content/docs/plugins/organization.mdx.
Describe alternatives you've considered
- Overloading
update-member-rolewith a confirmation branch for the creator role — rejected: that endpoint handles arbitrary multi-role updates for any member, not just ownership; bolting an async email-confirmation branch onto it changes its contract in a confusing, role-specific way and complicates testing/typing for a case that's a small minority of its calls. - A single generic "confirm sensitive org action" token type reused across delete/transfer — rejected for the initial version: the two actions have different payloads (org vs. two members) and different recipients (acting member vs. current owner); a shared abstraction can be extracted later if a third use case shows up, but building it now would be speculative.
Additional context
Existing prior art in this codebase to follow for both the API shape and the
security details (atomic single-use token consumption via
consumeVerificationValue, session-freshness bypass on the callback via
disableCookieCache, originCheck middleware on the callback URL):
packages/better-auth/src/api/routes/update-user.ts(deleteUser,deleteUserCallback)- Options contract:
packages/core/src/types/init-options.ts(user.deleteUser)
Related existing organization primitives this feature builds on:
packages/better-auth/src/plugins/organization/routes/crud-org.ts(deleteOrganization,beforeDeleteOrganization/afterDeleteOrganizationhooks)packages/better-auth/src/plugins/organization/routes/crud-members.ts(updateMemberRole, existing last-owner protection logic)
Source: better-auth/better-auth