better-auth-invite-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +629 -0
- package/dist/body.d.ts +24 -0
- package/dist/body.js +137 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +10 -0
- package/dist/hooks.d.ts +6 -0
- package/dist/hooks.js +122 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.js +41 -0
- package/dist/routes.d.ts +168 -0
- package/dist/routes.js +324 -0
- package/dist/schema.d.ts +76 -0
- package/dist/schema.js +37 -0
- package/dist/schemas.d.ts +24 -0
- package/dist/schemas.js +127 -0
- package/dist/types.d.ts +184 -0
- package/dist/types.js +14 -0
- package/dist/utils.d.ts +82 -0
- package/dist/utils.js +425 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sandy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
|
|
2
|
+
# 👥 Better Auth Invite Plugin
|
|
3
|
+
|
|
4
|
+
[](https://choosealicense.com/licenses/mit/)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://www.npmjs.com/package/better-auth-invite-plugin/)
|
|
7
|
+
[](https://www.better-auth.com/docs/concepts/plugins)
|
|
8
|
+
|
|
9
|
+
A plugin for Better Auth that adds an invitation system, allowing you to create, send, and manage invites for user sign-ups or role upgrades.
|
|
10
|
+
|
|
11
|
+
## ⚙️ Features
|
|
12
|
+
|
|
13
|
+
- Keep track of who created and who accepted the invite.
|
|
14
|
+
- Create and manage invitation codes to control user sign-ups.
|
|
15
|
+
- Send invitations via email, provide a shareable URL, or generate an invitation code.
|
|
16
|
+
- Automatically assign or upgrade roles when invites are used.
|
|
17
|
+
- Track each invitation's usage and enforce maximum uses.
|
|
18
|
+
- Support multiple token types, including default, code, or custom tokens.
|
|
19
|
+
- Store tokens securely in browser cookies for seamless activation.
|
|
20
|
+
- Fully customize behavior for redirects, token expiration, and email handling.
|
|
21
|
+
- Built with security in mind to prevent unauthorized invite usage.
|
|
22
|
+
- Show the invitee a welcome page or role upgrade page after signing up or upgrading their role.
|
|
23
|
+
## 💾 Installation
|
|
24
|
+
|
|
25
|
+
Install the plugin
|
|
26
|
+
|
|
27
|
+
<details>
|
|
28
|
+
<summary>npm</summary>
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install better-auth-invite-plugin
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
</details>
|
|
35
|
+
|
|
36
|
+
<details>
|
|
37
|
+
<summary>pnpm</summary>
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pnpm add better-auth-invite-plugin
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
</details>
|
|
44
|
+
|
|
45
|
+
<details>
|
|
46
|
+
<summary>yarn</summary>
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
yarn add better-auth-invite-plugin
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
</details>
|
|
53
|
+
|
|
54
|
+
## 🗄️ Server-Side Setup
|
|
55
|
+
|
|
56
|
+
Start by importing `invite` in your `betterAuth` configuration.
|
|
57
|
+
|
|
58
|
+
**Important:** `defaultRoleForSignupWithoutInvite` from the invite plugin should match `defaultRole` from the admin plugin
|
|
59
|
+
|
|
60
|
+
Basic example:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import invite from "better-auth-invite-plugin";
|
|
64
|
+
|
|
65
|
+
export const auth = betterAuth({
|
|
66
|
+
//... other options
|
|
67
|
+
database,
|
|
68
|
+
plugins: {
|
|
69
|
+
adminPlugin({
|
|
70
|
+
ac,
|
|
71
|
+
roles: { user, admin },
|
|
72
|
+
defaultRole: "user",
|
|
73
|
+
}),
|
|
74
|
+
invite({
|
|
75
|
+
defaultRoleForSignupWithoutInvite: "user",
|
|
76
|
+
defaultMaxUses: 1,
|
|
77
|
+
defaultRedirectToSignUp: "/auth/sign-up",
|
|
78
|
+
defaultRedirectToSignIn: "/auth/sign-in",
|
|
79
|
+
defaultRedirectAfterUpgrade: "/auth/invited",
|
|
80
|
+
sendUserInvitation: async ({ email, role, url }) => {
|
|
81
|
+
void sendInvitationEmail(role as RoleType, email, url);
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
emailAndPassword: {
|
|
86
|
+
enabled: true
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If you want, you can use a lot more options, here's an example using all the options
|
|
92
|
+
|
|
93
|
+
<details>
|
|
94
|
+
<summary>Advanced example</summary>
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import invite from "better-auth-invite-plugin";
|
|
98
|
+
|
|
99
|
+
export const auth = betterAuth({
|
|
100
|
+
//... other options
|
|
101
|
+
database,
|
|
102
|
+
plugins: {
|
|
103
|
+
adminPlugin({
|
|
104
|
+
ac,
|
|
105
|
+
roles: { user, admin, owner },
|
|
106
|
+
defaultRole: "user",
|
|
107
|
+
}),
|
|
108
|
+
invite({
|
|
109
|
+
defaultRoleForSignupWithoutInvite: "user", // The default role for users that signed up without an invite
|
|
110
|
+
getDate: () => new Date(), // Don't know why would you change the getDate function, but you can if you want
|
|
111
|
+
canCreateInvite: (inviteUser, inviterUser) => { // Can a user create an invite? By default it checks that the inviter role isn't defaultRoleForSignupWithoutInvite
|
|
112
|
+
if (!inviterUser.role) return false;
|
|
113
|
+
|
|
114
|
+
const RoleHierarchy = {
|
|
115
|
+
user: 1,
|
|
116
|
+
admin: 2,
|
|
117
|
+
owner: 3,
|
|
118
|
+
} as const;
|
|
119
|
+
|
|
120
|
+
return ( // If the inviter isn't trying to invite a user with a higer role than his, he can create the invite
|
|
121
|
+
RoleHierarchy[inviterUser.role as RoleType] >=
|
|
122
|
+
RoleHierarchy[inviteUser.role as RoleType]
|
|
123
|
+
);
|
|
124
|
+
},
|
|
125
|
+
canAcceptInvite: ({ user, newAccount }) => { // Can a user accept an invite? By default he can accept an invite
|
|
126
|
+
return newAccount; // Can only accept an invite to create an account, they cannot upgrade their role
|
|
127
|
+
},
|
|
128
|
+
generateToken: () => { // If you want you can create your own custom tokens
|
|
129
|
+
return String(Math.floor(Math.random() * 9) + 1); // Totally not ideal, since this only allows 9 different tokens
|
|
130
|
+
}
|
|
131
|
+
defaultTokenType: "token", // Token is recomended for email invites
|
|
132
|
+
defaultRedirectToSignUp: "/auth/sign-up", // The url to sign up the user
|
|
133
|
+
defaultRedirectToSignIn: "/auth/sign-in", // The url to sign in the user
|
|
134
|
+
defaultRedirectAfterUpgrade: "/auth/invited", // You should put a welcome message on this page
|
|
135
|
+
defaultShareInviterName: true, // Will be passed as an argument to the defaultRedirectAfterUpgrade
|
|
136
|
+
defaultMaxUses: 1,
|
|
137
|
+
defaultSenderResponse: "url", // If no email is provider, the sender will receive a URL to share with friends!
|
|
138
|
+
defaultSenderResponseRedirect: "signUp", // If no email is provided and defaultSenderResponse is "url", the user will be redirected to the sign-up page when they open that URL
|
|
139
|
+
customCookiePrefix: "my-app", // It defaults to better auth config advanced.cookiePrefix or simply "better-auth"
|
|
140
|
+
customCookieName: "{prefix}_invite", // Change your cookie name, you can use {prefix}, it'll be replaced with customCookiePrefix
|
|
141
|
+
sendUserInvitation: async ({ email, role, url }) => { // Implement your logic to send an email
|
|
142
|
+
void sendInvitationEmail(role as RoleType, email, url);
|
|
143
|
+
},
|
|
144
|
+
invitationTokenExpiresIn: 3600, // The token is only valid for 1 hour (in seconds)
|
|
145
|
+
onInvitationUsed: async ({ user, newAccount }) => {
|
|
146
|
+
// Send the user an email after they use an invitation
|
|
147
|
+
if (newAccount) // If it's a new account send them a welcome email
|
|
148
|
+
return void sendWelcomeEmail(user.name, user.email);
|
|
149
|
+
|
|
150
|
+
// If it's not a new account, send them an email telling them their new role
|
|
151
|
+
void sendRoleUpgraeEmail(user.name, user.email, user.role);
|
|
152
|
+
},
|
|
153
|
+
schema: { // Customize the table and column names
|
|
154
|
+
invite: {
|
|
155
|
+
fields: {
|
|
156
|
+
token: "invite_token"
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
},
|
|
162
|
+
emailAndPassword: {
|
|
163
|
+
enabled: true
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
</details>
|
|
169
|
+
|
|
170
|
+
## ⚙️ Invite Options
|
|
171
|
+
|
|
172
|
+
<details>
|
|
173
|
+
<summary>Click to expand</summary>
|
|
174
|
+
|
|
175
|
+
? = Optional
|
|
176
|
+
|
|
177
|
+
| Property | Type | Description | Default |
|
|
178
|
+
| :------- | :--- | :---------- | :------ |
|
|
179
|
+
| `defaultRoleForSignupWithoutInvite` | `string` | The role that users signing up without an invitation should have. | — |
|
|
180
|
+
| `getDate?` | `() => Date` | Function to generate the date. | `() => new Date()` |
|
|
181
|
+
| `canCreateInvite?` | `(inviteUser: {email?: string; role: string}, inviterUser: UserWithRole) => boolean` | Function that runs before creating an invite. | — |
|
|
182
|
+
| `canAcceptInvite?` | `(data: {user: UserWithRole, newAccount: boolean}) => boolean` | Function that runs before accepting an invite. | — |
|
|
183
|
+
| `generateToken?` | `() => string` | Function to generate a custom token. | — |
|
|
184
|
+
| `defaultTokenType?` | `"token" \| "code" \| "custom"` | Default token type. | `token` |
|
|
185
|
+
| `defaultRedirectToSignUp` | `string` | URL to redirect the user to sign up. | — |
|
|
186
|
+
| `defaultRedirectToSignIn` | `string` | URL to redirect the user to sign in. | — |
|
|
187
|
+
| `defaultRedirectAfterUpgrade` | `string` | URL to redirect the user after upgrading role. | — |
|
|
188
|
+
| `defaultShareInviterName?` | `boolean` | Whether the inviter's name is shared with the invitee by default. | `true` |
|
|
189
|
+
| `defaultMaxUses` | `number` | Max number of uses for an invite. | `1` |
|
|
190
|
+
| `defaultSenderResponse?` | `"token" \| "url"` | How the sender receives the token if no email is provided. | `token` |
|
|
191
|
+
| `defaultSenderResponseRedirect?` | `"signUp" \| "signIn"` | Where to redirect the user if no email is provided. | `signUp` |
|
|
192
|
+
| `customCookiePrefix?` | `string` | Custom cookie prefix. | `better-auth` |
|
|
193
|
+
| `customCookieName?` | `string` | Custom cookie name, `{prefix}` can be used. | `{prefix}.invite-token` |
|
|
194
|
+
| `sendUserInvitation?` | `(data: {email: string; role: string; url: string; token: string}, request?: Request) => Promise<void>` | Function to send the invitation email. | — |
|
|
195
|
+
| `invitationTokenExpiresIn?` | `number` | Number of seconds the token is valid for. | `3600` |
|
|
196
|
+
| `onInvitationUsed?` | `(data: { user: UserWithRole, newAccount: boolean}, request?: Request) => Promise<void>` | Callback when an invite is used. | — |
|
|
197
|
+
| `schema?` | `InferOptionSchema<InviteSchema>` | Custom schema for the invite plugin. | — |
|
|
198
|
+
|
|
199
|
+
</details>
|
|
200
|
+
|
|
201
|
+
## 🖥️ Client-Side Setup
|
|
202
|
+
|
|
203
|
+
Import the `inviteClient` plugin and add it to your `betterAuth` configuration.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { inviteClient } from "better-auth-invite-plugin";
|
|
207
|
+
|
|
208
|
+
const client = createClient({
|
|
209
|
+
//... other options
|
|
210
|
+
plugins: [
|
|
211
|
+
inviteClient()
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## 💡 Usage/Examples
|
|
217
|
+
|
|
218
|
+
<h3 id="creating-invites"></h3>
|
|
219
|
+
|
|
220
|
+
### 1. Creating Invites
|
|
221
|
+
Authenticated users can create invite codes. You can create an invite on the client or on the server.
|
|
222
|
+
|
|
223
|
+
<details>
|
|
224
|
+
<summary>Create an invite on the server</summary>
|
|
225
|
+
|
|
226
|
+
Use `authClient.invite.create` to create one.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { client } from "@/lib/auth-client";
|
|
230
|
+
|
|
231
|
+
const { data, error } = await client.invite.create({
|
|
232
|
+
// Here you put the options
|
|
233
|
+
role: "admin"
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (error) {
|
|
237
|
+
console.error("Failed to create invite:", error);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (data) {
|
|
242
|
+
console.log("Invite token created:", data.token);
|
|
243
|
+
// Example response: { data: { status: true, message: "test", token: "invite-123" }, error: null }
|
|
244
|
+
return data.token;
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
</details>
|
|
248
|
+
|
|
249
|
+
<details>
|
|
250
|
+
<summary>Create an invite on the client</summary>
|
|
251
|
+
|
|
252
|
+
Use `authClient.invite.create` to create one.
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { auth } from "@/lib/auth";
|
|
256
|
+
|
|
257
|
+
const { data, error } = await auth.api.createInvite({
|
|
258
|
+
headers: ..., // The user headers, req.headers on api, await headers() on NextJS
|
|
259
|
+
body: { // In the body you put the options
|
|
260
|
+
role: "admin"
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (error) {
|
|
265
|
+
console.error("Failed to create invite:", error);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (data) {
|
|
270
|
+
console.log("Invite token created:", data.token);
|
|
271
|
+
// Example response: { data: { status: true, message: "test", token: "invite-123" }, error: null }
|
|
272
|
+
return data.token;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
</details>
|
|
276
|
+
|
|
277
|
+
#### [Create invite options](#create-invite-options)
|
|
278
|
+
|
|
279
|
+
<h3 id="activating-invites"></h3>
|
|
280
|
+
|
|
281
|
+
### 2. Activating Invites
|
|
282
|
+
|
|
283
|
+
When a user receives an invite code, he needs to activate it.
|
|
284
|
+
If the user recives an email, the link they recive automaticly activates the invite.
|
|
285
|
+
Also you can activate an invite on the client or on the server (manually).
|
|
286
|
+
|
|
287
|
+
<details>
|
|
288
|
+
<summary>Activating an invite manually</summary>
|
|
289
|
+
|
|
290
|
+
To manually activate an invite, you can use one of the following methods depending on whether you are working on the server or client side.
|
|
291
|
+
|
|
292
|
+
<details>
|
|
293
|
+
<summary>Create an invite on the server</summary>
|
|
294
|
+
|
|
295
|
+
Use `authClient.invite.activate` to create one.
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import { client } from "@/lib/auth-client";
|
|
299
|
+
|
|
300
|
+
async function activateInvite(token: string) {
|
|
301
|
+
const { data, error } = await client.invite.activate({
|
|
302
|
+
token,
|
|
303
|
+
callbackURL: "/auth/sign-up" // Where to redirect the user
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (error) {
|
|
307
|
+
console.error("Failed to activate invite:", error);
|
|
308
|
+
// Handle error (e.g., code invalid, expired, already used)
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// On successful activation, a cookie named '{your-app-name}.invite-code'
|
|
313
|
+
// is set in the user's browser. This cookie will be used during sign-up.
|
|
314
|
+
console.log("Invite activated successfully.");
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
</details>
|
|
319
|
+
|
|
320
|
+
<details>
|
|
321
|
+
<summary>Create an invite on the client</summary>
|
|
322
|
+
|
|
323
|
+
Use `auth.api.activateInvite` to create one.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import { auth } from "@/lib/auth";
|
|
327
|
+
|
|
328
|
+
async function activateInvite(token: string) {
|
|
329
|
+
const { data, error } = await auth.api.activateInvite({
|
|
330
|
+
headers: ..., // The usera headers, req.headers on api, await headers() on NextJS
|
|
331
|
+
body: { // In the body you put the options
|
|
332
|
+
token,
|
|
333
|
+
callbackURL: "/auth/sign-up" // Where to redirect the user
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (error) {
|
|
338
|
+
console.error("Failed to activate invite:", error);
|
|
339
|
+
// Handle error (e.g., code invalid, expired, already used)
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// On successful activation, a cookie named '{your-app-name}.invite-code'
|
|
344
|
+
// is set in the user's browser. This cookie will be used during sign-up.
|
|
345
|
+
console.log("Invite activated successfully.");
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
</details>
|
|
350
|
+
|
|
351
|
+
</details>
|
|
352
|
+
|
|
353
|
+
<details>
|
|
354
|
+
<summary>Activating an invite automaticly</summary>
|
|
355
|
+
|
|
356
|
+
When you create an invite (see ["Creating Invites"](#creating-invites)) and provide an email.
|
|
357
|
+
|
|
358
|
+
<details>
|
|
359
|
+
<summary>Example</summary>
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
import { client } from "@/lib/auth-client";
|
|
363
|
+
|
|
364
|
+
const { data, error } = await client.invite.create({
|
|
365
|
+
email: "test@test.com"
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
</details>
|
|
369
|
+
|
|
370
|
+
An email will be sent to that user. The system checks whether a user with that email already exists:
|
|
371
|
+
|
|
372
|
+
- If the user does **not** exist, the email invites them to **sign up** and create an account.
|
|
373
|
+
- If the user **already exists**, the email invites them to **upgrade their role**.
|
|
374
|
+
|
|
375
|
+
When the user follows the link, the token is automatically activated. After completing the action (creating an account or upgrading their role), they are redirected to `redirectToAfterUpgrade` to see their new account or updated role.
|
|
376
|
+
</details>
|
|
377
|
+
|
|
378
|
+
#### [Activate invite options](#activate-invite-options)
|
|
379
|
+
|
|
380
|
+
### 3. Signing up
|
|
381
|
+
|
|
382
|
+
The invite system works alongside the standard sign-up and sign-in flow. The outcome depends on whether the user has an active invite.
|
|
383
|
+
|
|
384
|
+
#### How it works
|
|
385
|
+
|
|
386
|
+
- When an invite is activated, the token is saved in the user's browser cookie.
|
|
387
|
+
- A hook runs after key authentication endpoints (like `/sign-up/email`, `/sign-in/email`, `/verify-email`, and social callbacks).
|
|
388
|
+
- The hook validates the token, checks expiration and max uses, and marks the invite as used.
|
|
389
|
+
- The user's role is upgraded if applicable.
|
|
390
|
+
- The cookie is cleared after the invite is consumed.
|
|
391
|
+
- The user is redirected to `defaultRedirectAfterUpgrade` to see their new role or welcome page.
|
|
392
|
+
|
|
393
|
+
#### Scenario 1: Using an Active Invite
|
|
394
|
+
|
|
395
|
+
1. **Activate Invite:** The user activates an invite code (see ["Activating Invites"](#activating-invites)), which sets a `{your-app-name}.invite-code` cookie.
|
|
396
|
+
2. **Sign Up / Sign In:** The user completes the normal sign-up or sign-in process (e.g., using email and password).
|
|
397
|
+
3. **Role Upgrade:** If a valid `{your-app-name}.invite-code` cookie exists:
|
|
398
|
+
- The invite is validated.
|
|
399
|
+
- The user's role is upgraded.
|
|
400
|
+
- The invite is marked as used in the database.
|
|
401
|
+
- The `{your-app-name}.invite-code` cookie is cleared.
|
|
402
|
+
|
|
403
|
+
<details>
|
|
404
|
+
<summary>Example Code</summary>
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
import { client } from "@/lib/auth-client";
|
|
408
|
+
|
|
409
|
+
// Using the client with an active invite
|
|
410
|
+
async function signUpWithInvite(email, password, name) {
|
|
411
|
+
const { data, error } = await client.signUp.email({ email, password, name });
|
|
412
|
+
|
|
413
|
+
if (error) {
|
|
414
|
+
console.error("Sign-up failed:", error);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (data) {
|
|
419
|
+
console.log(
|
|
420
|
+
"Sign-up successful. Role should now be updated:",
|
|
421
|
+
data.user
|
|
422
|
+
);
|
|
423
|
+
// data.user contains the new user object with the upgraded role
|
|
424
|
+
// data.token contains the session token
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
</details>
|
|
429
|
+
|
|
430
|
+
#### Scenario 2: Without an Invite
|
|
431
|
+
|
|
432
|
+
1. **Sign Up:** The user signs up without activating an invite or if the activated invite is invalid, expired, or already used.
|
|
433
|
+
2. **Default Role:**
|
|
434
|
+
- The user is created with the default role defined in the admin plugin.
|
|
435
|
+
- The invite system does not affect this user.
|
|
436
|
+
|
|
437
|
+
This approach allows capturing user interest even if invites are limited. Roles can be upgraded later using a valid invite or administrative actions.
|
|
438
|
+
|
|
439
|
+
<details>
|
|
440
|
+
<summary>Example Code</summary>
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
import { client } from "@/lib/auth-client";
|
|
444
|
+
|
|
445
|
+
// Using the client without an invite
|
|
446
|
+
async function signUpWithoutInvite(email, password, name) {
|
|
447
|
+
const { data, error } = await client.signUp.email({ email, password, name });
|
|
448
|
+
|
|
449
|
+
if (error) {
|
|
450
|
+
console.error("Sign-up failed:", error);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (data) {
|
|
455
|
+
console.log(
|
|
456
|
+
"Sign-up successful. Role should be roleForSignupWithoutInvite:",
|
|
457
|
+
data.user
|
|
458
|
+
);
|
|
459
|
+
// data.user contains the new user object with the default role
|
|
460
|
+
// data.token contains the session token
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
</details>
|
|
465
|
+
|
|
466
|
+
## 🔒 Security
|
|
467
|
+
|
|
468
|
+
The invite system provides several built-in mechanisms to ensure secure management of invitations and role upgrades.
|
|
469
|
+
|
|
470
|
+
### Token Types
|
|
471
|
+
|
|
472
|
+
Each invite has a token, which is used to validate and track its use:
|
|
473
|
+
|
|
474
|
+
- Token - Default type. A long, random string generated with generateId(24) from better-auth. Recommended for email and url invitations.
|
|
475
|
+
- Code - Shorter, human-readable codes. Generated with random alphanumeric characters. Use with care, as they are easier to guess.
|
|
476
|
+
- Custom - Allows defining your own token generator function via generateToken.
|
|
477
|
+
|
|
478
|
+
### Invite Permissions
|
|
479
|
+
|
|
480
|
+
- Creating invites: By default, a user cannot create an invite if their role is defaultRoleForSignupWithoutInvite.
|
|
481
|
+
- This behavior can be customized using the canCreateInvite function in the invite options.
|
|
482
|
+
|
|
483
|
+
### Accepting invites
|
|
484
|
+
|
|
485
|
+
- By default, all users can accept invites
|
|
486
|
+
- This behavior can be customized using the canAcceptInvite function in the invite options.
|
|
487
|
+
|
|
488
|
+
### Expiration and Usage Limits
|
|
489
|
+
|
|
490
|
+
- Each invite has an expiration date (expiresAt) and a maximum number of uses (maxUses), preventing unlimited sharing or misuse.
|
|
491
|
+
- Expired invites or invites that have reached their usage limit are automatically rejected and deleted.
|
|
492
|
+
|
|
493
|
+
### Secure Delivery
|
|
494
|
+
|
|
495
|
+
- When sending invites via email, the system uses configured functions (sendUserInvitation or sendUserRoleUpgrade) to securely deliver the token or link.
|
|
496
|
+
- Invites that were send to a certain email can only be used by that exact email.
|
|
497
|
+
- The token is stored in an HTTP-only cookie when the invite is activated, protecting it from client-side access.
|
|
498
|
+
|
|
499
|
+
### Best Practices
|
|
500
|
+
|
|
501
|
+
- Use the default token type for most invitations, as it provides the highest entropy.
|
|
502
|
+
- Ensure canCreateInvite is properly configured to prevent users from inviting others to roles above their own.
|
|
503
|
+
- Never expose the token in client-side code or logs.
|
|
504
|
+
- Monitor maxUses and expiresAt to avoid old invites being exploited.
|
|
505
|
+
## 🧩 API Reference
|
|
506
|
+
|
|
507
|
+
<details>
|
|
508
|
+
<summary>POST /invite/create (Create an invite)</summary>
|
|
509
|
+
|
|
510
|
+
It creates a token using all the configuration, if you call this
|
|
511
|
+
endpoint with an email, it will send them an email to create their
|
|
512
|
+
account or to upgrade their role.
|
|
513
|
+
|
|
514
|
+
```http
|
|
515
|
+
POST /invite/create
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
<h3 id="create-invite-options"></h3>
|
|
519
|
+
? = Optional
|
|
520
|
+
|
|
521
|
+
| Parameter | Type | Description | Default value | Where |
|
|
522
|
+
| :-------- | :--- | :---------- | :------------ | :----- |
|
|
523
|
+
| `role` | `string` | The role to give the invited user. | — | `BODY` |
|
|
524
|
+
| `email?` | `email` | The email address of the user to send a invitation email to. | — | `BODY` |
|
|
525
|
+
| `tokenType?` | `"token" \| "code" \| "custom"` | Type of token to use, 24 character token, 6 digit code or custom options.generateToken. | `options.defaultTokenType` | `BODY` |
|
|
526
|
+
| `redirectToSignUp?` | `string` | The URL to redirect the user to create their account. | `options.defaultRedirectTo` | `BODY` |
|
|
527
|
+
| `redirectToSignIn?` | `string` | The URL to redirect the user to upgrade their role. | `options.defaultRedirectToSignIn` | `BODY` |
|
|
528
|
+
| `maxUses?` | `number` | The number of times an invitation can be used. | `options.defaultMaxUses` | `BODY` |
|
|
529
|
+
| `expiresIn?` | `number` | Number of seconds the invitation token is valid for. | `options.invitationTokenExpiresIn` | `BODY` |
|
|
530
|
+
| `redirectToAfterUpgrade?` | `string` | The URL to redirect the user to after upgrade their role (if the user is already logged in). | `options.defaultRedirectAfterUpgrade` | `BODY` |
|
|
531
|
+
| `shareInviterName?` | `boolean` | Whether the inviter's name should be shared with the invitee. | options.defaultShareInviterName | `BODY` |
|
|
532
|
+
| `senderResponse?` | `"token" \| "url"` | How should the sender receive the token (sender only receives a token if no email is provided) | `options.defaultSenderResponse` | `BODY` |
|
|
533
|
+
| `senderResponseRedirect?` | `"signUp" \| "signIn"` | Where should we redirect the user? (only if no email is provided) | `options.defaultSenderResponseRedirect` | `BODY` |
|
|
534
|
+
|
|
535
|
+
</details>
|
|
536
|
+
|
|
537
|
+
<details>
|
|
538
|
+
<summary>POST /invite/activate (Activate an invite)</summary>
|
|
539
|
+
|
|
540
|
+
It saves the token in the user's cookie, for later use in a hook,
|
|
541
|
+
but if user is already signed in, it will only consume the token
|
|
542
|
+
and upgrade their role.
|
|
543
|
+
|
|
544
|
+
```http
|
|
545
|
+
POST /invite/activate
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
<h3 id="activate-invite-options"></h3>
|
|
549
|
+
? = Optional
|
|
550
|
+
|
|
551
|
+
| Parameter | Type | Description | Default value | Where |
|
|
552
|
+
| :-------- | :--- | :---------- | :------------ | :----- |
|
|
553
|
+
| `token` | `string` | The invitation token. | — | `BODY` |
|
|
554
|
+
| `redirectTo?` | `string` | Where to redirect the user to sign in/up. | — | `BODY` |
|
|
555
|
+
|
|
556
|
+
</details>
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
<details>
|
|
560
|
+
<summary>GET /invite/:token (Activate an invite callback)</summary>
|
|
561
|
+
|
|
562
|
+
It saves the token in the user's cookie, for later use in a hook,
|
|
563
|
+
but if user is already signed in, it will only consume the token
|
|
564
|
+
and upgrade their role.
|
|
565
|
+
|
|
566
|
+
This endpoint is meant to be used as a **callback**.
|
|
567
|
+
It is the URL sent in invitation emails and when senderResponse is set to "url"
|
|
568
|
+
|
|
569
|
+
Unlike `POST /invite/activate`, this endpoint uses **GET**, and is intended to be called via a URL and should not be called directly from the API (auth.api.activateInvite or authClient.invite.activate)
|
|
570
|
+
|
|
571
|
+
```http
|
|
572
|
+
GET /invite/:token
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
<h3 id="activate-invite-options"></h3>
|
|
576
|
+
? = Optional
|
|
577
|
+
|
|
578
|
+
| Parameter | Type | Description | Default value | Where |
|
|
579
|
+
| :-------- | :--- | :---------- | :------------ | :----- |
|
|
580
|
+
| `token` | `string` | The invitation token. | — | `URL` |
|
|
581
|
+
| `redirectTo?` | `string` | Where to redirect the user to sign in/up. | — | `QUERY` |
|
|
582
|
+
|
|
583
|
+
</details>
|
|
584
|
+
|
|
585
|
+
## Database Schema
|
|
586
|
+
|
|
587
|
+
<details>
|
|
588
|
+
<summary>Invite Table</summary>
|
|
589
|
+
|
|
590
|
+
? = optional
|
|
591
|
+
|
|
592
|
+
### `invite` table
|
|
593
|
+
|
|
594
|
+
| Column | Type | Description | References |
|
|
595
|
+
|--------|------|-------------|------------|
|
|
596
|
+
| `token` | `string` | The unique invite code. | — |
|
|
597
|
+
| `createdAt` | `date` | Timestamp when the invite was created. | — |
|
|
598
|
+
| `expiresAt` | `date` | Timestamp when the invite expires. | — |
|
|
599
|
+
| `maxUses` | `number` | Maximum number of times the invite can be used. | — |
|
|
600
|
+
| `createdByUserId?` | `string` | ID of the user who created the invite. | `user.id` |
|
|
601
|
+
| `callbackURL` | `string` | URL to redirect the user after activating the invite. | — |
|
|
602
|
+
| `redirectToAfterUpgrade` | `string` | URL to redirect the user after their role is upgraded. | — |
|
|
603
|
+
| `shareInviterName` | `boolean` | Whether to share the inviter's name with the invitee. | — |
|
|
604
|
+
| `email?` | `string` | Email of the invited user. Optional if sending a URL directly. | — |
|
|
605
|
+
| `role` | `string` | Role to assign to the invited user. | — |
|
|
606
|
+
</details>
|
|
607
|
+
|
|
608
|
+
<details>
|
|
609
|
+
<summary>InviteUse Table</summary>
|
|
610
|
+
|
|
611
|
+
? = optional
|
|
612
|
+
|
|
613
|
+
### `invite_use` table
|
|
614
|
+
|
|
615
|
+
| Column | Type | Description | References |
|
|
616
|
+
|--------|------|-------------|------------|
|
|
617
|
+
| `inviteId` | `string` | ID of the invite being used. | `invite.token` |
|
|
618
|
+
| `usedAt` | `date` | Timestamp when the invite was used. | — |
|
|
619
|
+
| `usedByUserId?` | `string` | ID of the user who used the invite. | `user.id` |
|
|
620
|
+
</details>
|
|
621
|
+
|
|
622
|
+
## Acknowledgements
|
|
623
|
+
|
|
624
|
+
- Partially based in [better auth invite from bard](https://github.com/bard/better-auth-invite)
|
|
625
|
+
|
|
626
|
+
## License
|
|
627
|
+
|
|
628
|
+
[MIT](https://choosealicense.com/licenses/mit/)
|
|
629
|
+
|
package/dist/body.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
export declare const createInviteBodySchema: z.ZodObject<{
|
|
3
|
+
role: z.ZodString;
|
|
4
|
+
email: z.ZodOptional<z.ZodEmail>;
|
|
5
|
+
tokenType: z.ZodOptional<z.ZodEnum<{
|
|
6
|
+
token: "token";
|
|
7
|
+
code: "code";
|
|
8
|
+
custom: "custom";
|
|
9
|
+
}>>;
|
|
10
|
+
redirectToSignUp: z.ZodOptional<z.ZodString>;
|
|
11
|
+
redirectToSignIn: z.ZodOptional<z.ZodString>;
|
|
12
|
+
maxUses: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
expiresIn: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
redirectToAfterUpgrade: z.ZodOptional<z.ZodString>;
|
|
15
|
+
shareInviterName: z.ZodOptional<z.ZodBoolean>;
|
|
16
|
+
senderResponse: z.ZodOptional<z.ZodEnum<{
|
|
17
|
+
token: "token";
|
|
18
|
+
url: "url";
|
|
19
|
+
}>>;
|
|
20
|
+
senderResponseRedirect: z.ZodOptional<z.ZodEnum<{
|
|
21
|
+
signUp: "signUp";
|
|
22
|
+
signIn: "signIn";
|
|
23
|
+
}>>;
|
|
24
|
+
}, z.core.$strip>;
|