How it works
The lifecycle of an invitation, from create to redeem.
The lifecycle
Someone creates an invite
An admin (or an org member with permission) creates it. Private invites are emailed by your sendPrivateInvitation; public invites return a shareable URL once. A private invite link is never shown to its creator: only the emailed recipient holds it, which is why accepting one marks the email verified.
The invitee lands on your invite page
Every invite link points at one page in your app carrying only ?token=. The page calls invite.get, renders what nextAction and requiredFields say, and submits to invite.redeem.
Redemption grants access
In invite-only mode redemption creates the user and their credential account. In open mode it merges the invited role (and org membership) into the signed-in user's account. Roles always merge as a union, so an invite can never demote anyone. Redemption never creates or mutates a session: the accepter signs in through your normal flow with the credentials they just set, and your app decides the active organization there.
The record stays behind
Every redemption writes an append-only inviteUse row, and accepted invites are permanent audit records: who invited whom, and who redeemed what.
Email verification by design
The two delivery types prove different things, and emailVerified follows the proof:
- Private invites pre-create an inert, unverified user, locking the address. The link is never shown to its creator, so the token only ever exists in the recipient's mailbox; redeeming it is proof of mailbox access, and the email is marked verified on accept.
- Public invites pre-create nothing. The accepter types their own email, and since holding a shared link proves nothing about a mailbox, the account is created unverified. The plugin sends Better Auth's standard verification email (when
sendOnSignUporrequireEmailVerificationis configured), and the user verifies by clicking it like any other signup.
Recommended
Keep requireEmailVerification: true in your auth.ts unless you want unverified users signing
in. With it, a public-invite accepter cannot sign in until they click the verification link;
private-invite accepters are already verified by the invite itself.
With that setting, the emailed link is the invitee's only way in, so this piece of auth.ts is load-bearing:
export const auth = betterAuth({
emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
await sendEmail(user.email, "Verify your email", url);
},
sendOnSignUp: true // also fires for public-invite redemptions
},
emailAndPassword: {
enabled: true,
requireEmailVerification: true // unverified users cannot sign in
}
});Redemption itself never signs anyone in: after ACCEPTED, the user signs in through your normal flow with the credentials they just set. See The invite page. And when an invitation email goes missing, invite.resend invalidates the old link and delivers a fresh one.
The guarantees
- A database leak exposes no usable links. Tokens are crypto-random and stored SHA-256 hashed; the raw token is never persisted.
- Exactly one winner. All state changes are guarded atomic writes: parallel redemptions of a one-seat invite produce exactly one winner.
- No cron, nothing to sweep. Expiry is derived from
expiresAtat read time. - Pending invites lock their email. While a private invite is pending, its email is locked on every path: sign-in, sign-up, password reset, and OAuth linking are all blocked without leaking that the invite exists.
Role changes made outside this plugin do not merge
The union merge above applies only to roles granted through invite redemption. Better Auth stores
multiple roles as one comma-separated string (for example "user,org-creator"), and other role
writers replace that string wholesale. The admin plugin's setRole is the usual culprit: it
overwrites the field with exactly what you send, so calling it with just "admin" silently strips
roles the user gained through invites. When you change roles from your own admin UI, always send
the full set. See the recipe.
Where to go next
Last updated on