# introduction: Introduction
URL: /docs/introduction
Source: https://raw.githubusercontent.com/better-auth-extended/better-auth-extended/refs/heads/main/apps/www/content/docs/introduction.mdx
Information about better-auth-extended
***
title: Introduction
description: Information about better-auth-extended
---------------------------------------------------
import { owner, repo } from "@/lib/github";
Welcome to the documentation for better-auth-extended! 👀
## What is better-auth-extended?
better-auth-extended is a curated collection of plugins, libraries and examples all open source for the community to freely use on top of [Better Auth](https://github.com/better-auth/better-auth).
The project is mainted by , and is not officially affiliated with Better Auth.
## What is Better Auth?
Better Auth is the most comprehensive authentication library for TypeScript. You can learn more about it [here](https://github.com/better-auth/better-auth).
## Why better-auth-extended?
This library is meant as a successor to 's [Better-Auth-Kit](https://github.com/ping-maxwell/better-auth-kit) which has been archived. The purpose of the library are features like plugins and libraries for Better-Auth that don't fit in the core library.
## License
Everything in the better-auth-extended project is licensed under the MIT License
# libraries: Test Utils
URL: /docs/libraries/test-utils
Source: https://raw.githubusercontent.com/better-auth-extended/better-auth-extended/refs/heads/main/apps/www/content/docs/libraries/test-utils.mdx
A collection of utilities to help you test your Better-Auth plugins.
***
title: Test Utils
description: A collection of utilities to help you test your Better-Auth plugins.
packageName: "@better-auth-extended/test-utils"
-----------------------------------------------
This library comes with test utilities to assist you in writing code to test your Better-Auth plugins.
## Installation
npm
pnpm
yarn
bun
```bash
npm install @better-auth-extended/test-utils
```
```bash
pnpm add @better-auth-extended/test-utils
```
```bash
yarn add @better-auth-extended/test-utils
```
```bash
bun add @better-auth-extended/test-utils
```
## Usage
```ts title="plugin.test.ts"
import { betterAuth } from "better-auth";
import { getTestInstance } from "@better-auth-extended/test-utils";
import { myPlugin, myPluginClient } from "./my-plugin";
const { auth, db, client, testUser, signUpWithTestUser } =
await getTestInstance({
options: {
database: new Database(), // Your database adapter
plugins: [myPlugin()],
},
clientOptions: {
plugins: [myPluginClient()],
},
});
```
or
```ts title="plugin.test.ts"
import { betterAuth } from "better-auth";
import { getTestInstance } from "@better-auth-extended/test-utils";
import { myPlugin, myPluginClient } from "./my-plugin";
const auth = betterAuth({
database: new Database(), // Your database adapter
plugins: [myPlugin()],
secret: "better-auth.secret",
emailAndPassword: {
enabled: true,
autoSignIn: true,
},
rateLimit: {
enabled: false,
},
advanced: {
disableCSRFCheck: true,
cookies: {},
},
});
const { db, client, testUser, signUpWithTestUser } = await getTestInstance({
auth,
clientOptions: {
plugins: [myPluginClient()],
},
});
```
## Writing Tests
You can now use the test APIs to test your plugin:
```ts title="plugin.test.ts"
const { headers, user } = await signUpWithTestUser();
describe("My Plugin", () => {
it("should do something cool", async () => {
const result = await client.myPlugin.doSomethingCool();
expect(result).toBe(true);
});
});
```
Then run the tests, for example, using Vitest:
```bash
vitest foobar
```
## API
### `getTestInstance`
Optionally takes a single config object with options for the `betterAuth` instance and test instance configuration.
You can configure the betterAuth client instance inside the config object.
```ts
const {
auth,
db,
client,
testUser,
signUpWithTestUser,
signInWithTestUser,
signInWithUser,
cookieSetter,
customFetchImpl,
sessionSetter,
context,
resetDatabase,
} = await getTestInstance({
options: {
// Better-Auth options
plugins: [myPlugin()],
},
clientOptions: {
// Client options
plugins: [myPluginClient()],
},
});
```
### Options
* `options` - The options for the Better-Auth instance.
* `auth` - An existing Better-Auth instance to use instead of creating a new one.
* `clientOptions` - The options for the Better-Auth client instance.
* `port` - The baseURL port for the better-auth instance.
* `disableTestUser` - Whether to disable the test user.
* `testUser` - The test user to use for the test instance.
* `shouldRunMigrations` - Whether to run database migrations on initialization.
### Methods
* `auth` - The Better-Auth server instance.
* `client` - The Better-Auth client instance.
* `signUpWithTestUser` - Sign up with the premade test user.
* `signInWithTestUser` - Sign in with the premade test user.
* `signInWithUser` - Sign in with a custom user.
* `cookieSetter` - Set the cookie for the test instance.
* `customFetchImpl` - The custom fetch implementation for the test instance.
* `sessionSetter` - Set the session for the test instance.
* `resetDatabase` - Reset the database by clearing all auth tables.
***
#### `auth`
The Better-Auth server instance.
#### `client`
The Better-Auth client instance.
#### `testUser`
The premade test user.
#### `signUpWithTestUser`
```ts
const { headers, user, session, token } = await signUpWithTestUser();
```
#### `signInWithTestUser`
```ts
const { headers, user, session, token } = await signInWithTestUser();
```
#### `signInWithUser`
```ts
const { headers, user, session, token } = await signInWithUser(email, password);
```
#### `cookieSetter`
Useful for getting the session of a successful sign in and applying that to a new headers object's cookie.
```ts
const headers = new Headers();
await client.signIn.email(
{
email: testUser.email,
password: testUser.password,
},
{
onSuccess: cookieSetter(headers),
}
);
```
#### `customFetchImpl`
By default, when using the auth client, we make a fetch request to the better-auth server whenever you call an endpoint.
However, you can optionally provide the `customFetchImpl` to bypass this and it will skip the fetch request to the better-auth server, and instead directly invoke the endpoint on the server.
```ts
const client = createAuthClient({
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
},
});
```
#### `sessionSetter`
Useful for getting the session from the response of a successful sign in and applying that to a new headers object.
```ts
const headers = new Headers();
await client.signIn.email(
{
email: testUser.email,
password: testUser.password,
},
{
onSuccess: sessionSetter(headers),
}
);
const response = await client.listSessions({
fetchOptions: {
headers,
},
});
```
#### `context`
The Better-Auth context object.
#### `db`
The database adapter.
```ts title="example"
await db.create({
model: "sometable",
data: {
hello: "world",
},
});
```
#### `resetDatabase`
Reset the database by clearing all auth tables.
```ts
// Reset all auth tables
await resetDatabase();
// Reset specific tables
await resetDatabase(["user", "session"]);
```
# plugins: App Invite
URL: /docs/plugins/app-invite
Source: https://raw.githubusercontent.com/better-auth-extended/better-auth-extended/refs/heads/main/apps/www/content/docs/plugins/app-invite.mdx
Invite users to your application and allow them to sign up.
***
title: App Invite
description: Invite users to your application and allow them to sign up.
packageName: "@better-auth-extended/app-invite"
-----------------------------------------------
The App Invite plugin enables you to invite users to your application through email invitations. It supports two types of invitations:
* **Personal Invitations**: Targeted to specific email addresses, ensuring only the intended recipient can use the invitation
* **Public Invitations**: Can be used by multiple users, making it ideal for open sign-up scenarios
This plugin is particularly useful for invite-only applications.
## Installation
### Install the plugin
npm
pnpm
yarn
bun
```bash
npm install @better-auth-extended/app-invite
```
```bash
pnpm add @better-auth-extended/app-invite
```
```bash
yarn add @better-auth-extended/app-invite
```
```bash
bun add @better-auth-extended/app-invite
```
### Add the plugin to your auth config
To use the App Invite plugin, add it to your auth config.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { appInvite } from "@better-auth-extended/app-invite";
export const auth = betterAuth({
// ... other config options
plugins: [
appInvite({
// required for personal invites
sendInvitationEmail: (data) => {
// ... send invitation to the user
},
}),
],
});
```
### Add the client plugin
Include the App Invite client plugin in your authentication client instance.
```ts
import { createAuthClient } from "better-auth/client";
import { appInviteClient } from "@better-auth-extended/app-invite/client";
const authClient = createAuthClient({
plugins: [appInviteClient()],
});
```
### Run migrations
This plugin adds an additional table to the database. [Click here to see the schema](#schema)
npm
pnpm
yarn
bun
```bash
npx @better-auth/cli migrate
```
```bash
pnpm dlx @better-auth/cli migrate
```
```bash
yarn dlx @better-auth/cli migrate
```
```bash
bun x @better-auth/cli migrate
```
or generate
npm
pnpm
yarn
bun
```bash
npx @better-auth/cli generate
```
```bash
pnpm dlx @better-auth/cli generate
```
```bash
yarn dlx @better-auth/cli generate
```
```bash
bun x @better-auth/cli generate
```
## Usage
To add members to the application, we first need to send an invitation to the user.
The user will receive an email with the invitation link. Once the user accepts the invitation, they will be signed up to the application.
### Setup Invitation Email
For personal invites to work we first need to provide `sendInvitationEmail` to the `better-auth` instance.
This function is responsible for sending the invitation email to the user.
You'll need to construct and send the invitation link to the user. The link should include the invitation ID,
which will be used with the `acceptInvitation` function when the user clicks on it.
This is only required for personal invites. Sharing public invitations is up to the inviter.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { appInvite } from "@better-auth-extended/app-invite";
import { sendAppInvitation } from "./email";
export const auth = betterAuth({
plugins: [
appInvite({
async sendInvitationEmail(data) {
const inviteLink = `https://example.com/accept-invitation/${data.id}`;
sendAppInvitation({
name: data.name,
email: data.email,
invitedByUsername: data.inviter.name,
invitedByEmail: data.inviter.email,
inviteLink,
});
},
}),
],
});
```
### Send Invitation
To invite users to the app, you can use the `invite` function provided by the server and client.
### Client Side
```ts
const { data, error } = await authClient.inviteUser({
email, // required
resend, // required
domainWhitelist, // required
});
```
### Server Side
```ts
const data = await auth.api.inviteUser({
body: {
email, // required
resend, // required
domainWhitelist, // required
}
});
```
### Type Definition
```ts
type inviteUser = {
/**
* The email address of the user to invite. Leave empty to create a public invitation.
*/
email?: string
/**
* A boolean value that determines whether to resend the invitation email,
* if the user is already invited. Defaults to `false`
*/
resend?: boolean
/**
* An optional comma-separated list of domains that allows public invitations to be accepted only from approved domains (e.g., `example.com,*.example.org`).
*/
domainWhitelist?: string
}
```
### Accept Invitation
When a user receives an invitation email, they can click on the invitation link to accept the invitation.
The link should include the invitation ID, which will be used to accept the invitation.
### Client Side
```ts
const { data, error } = await authClient.acceptInvitation({
invitationId,
name, // required
email, // required
password,
});
```
### Server Side
```ts
const data = await auth.api.acceptAppInvitation({
body: {
invitationId,
name, // required
email, // required
password,
}
});
```
### Type Definition
```ts
type acceptAppInvitation = {
/**
* The ID of the invitation to accept
*/
invitationId: string
/**
* The name of the user that accepts the invitation. (overriden if predefined in the invitation)
*/
name?: string
/**
* The email address of the user that accepts the invitation. Required for public invites.
*/
email?: string
/**
* The password used to sign in after accepting the invitation.
*/
password: string
}
```
### Update Invitation Status
To update the status of invitations you can use the `acceptInvitation`, `rejectInvitation`, `cancelInvitation`
function provided by the client. The functions take the invitation id as an argument.
### Client Side
```ts
const { data, error } = await authClient.cancelInvitation({
invitationId,
});
```
### Server Side
```ts
const data = await auth.api.cancelAppInvitation({
body: {
invitationId,
}
});
```
### Type Definition
```ts
type cancelAppInvitation = {
/**
* The ID of the invitation to cancel.
*/
invitationId: string
}
```
### Client Side
```ts
const { data, error } = await authClient.rejectInvitation({
invitationId,
});
```
### Server Side
```ts
const data = await auth.api.rejectAppInvitation({
body: {
invitationId,
}
});
```
### Type Definition
```ts
type rejectAppInvitation = {
/**
* The ID of the invitation to reject.
*/
invitationId: string
}
```
### Get Invitation
To get an invitation you can use the `getAppInvitation` function provided by the client. You need to provide the
invitation id as a query parameter.
### Client Side
```ts
const { data, error } = await authClient.getAppInvitation({
id,
});
```
### Server Side
```ts
const data = await auth.api.getAppInvitation({
query: {
id,
}
});
```
### Type Definition
```ts
type getAppInvitation = {
/**
* The ID of the invitation to retrieve.
*/
id: string
}
```
### List Invitations
Allows a user to list all invitations issued by themselves.
By default 100 invitations are returned.
### Client Side
```ts
const { data, error } = await authClient.listInvitations({
searchField, // required
searchValue, // required
searchOperator, // required
limit, // required
offset, // required
sortBy, // required
sortDirection, // required
filterField, // required
filterValue, // required
filterOperator, // required
});
```
### Server Side
```ts
const data = await auth.api.listAppInvitation({
query: {
searchField, // required
searchValue, // required
searchOperator, // required
limit, // required
offset, // required
sortBy, // required
sortDirection, // required
filterField, // required
filterValue, // required
filterOperator, // required
}
});
```
### Type Definition
```ts
type listAppInvitation = {
/**
* The field to search on, which can be `email`, `name`, or `domainWhitelist`.
*/
searchField?: string
/**
* The value to search for.
*/
searchValue?: string
/**
* The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`
*/
searchOperator?: string
/**
* The number of invitations to return.
*/
limit?: number
/**
* The number of invitations to skip.
*/
offset?: number
/**
* The field to sort the invitations by.
*/
sortBy?: string
/**
* The direction to sort the invitations by. Defaults to `asc`.
*/
sortDirection?: string
/**
* The field to filter the invitations by.
*/
filterField?: string
/**
* The value to filter the invitations by.
*/
filterValue?: string
/**
* The operator to use for the filter. It can be `eq`, `ne`, `lt`, `lte`, `gt`, or `gte`.
*/
filterOperator?: string
}
```
## Schema
The plugin requires an additional table in the database.
Table Name: `appInvitation`
## Options
**canCreateInvitation**: `((ctx: GenericEndpointContext) => Promise | boolean | Promise | Permission)` | `boolean | Permission` - A function that determines whether a user can create an invitation. By default, it's `true`. You can set it to `false` to restrict users from creating invitations.
**canCancelInvitation** `((ctx: GenericEndpointContext, invite: AppInvitation) => Promise | boolean | Promise | Permission)` | `boolean | Permission` - A function that determines whether a user can cancel invitations. By default, the user can only cancel invites they created. You can set it to `false` to restrict users from canceling invitations.
Returning `Permission` requires the [admin plugin](https://www.better-auth.com/docs/plugins/admin) to be configured. If it's not set up, the request will fail.
Example:
```ts title="auth.ts"
canCancelInvitation: {
statement: "appInvite",
permissions: ["create"],
};
```
**sendInvitationEmail**: `async (data) => Promise` - A function that sends an invitation email to the user. This is only required for personal invitations.
**invitationExpiresIn**: `number` - How long the invitation link is valid for in seconds. By default an invitation expires after 48 hours (2 days). Set it to `null` to prevent invitations from expiring.
**autoSignIn**: `boolean` - A boolean value that determines whether to prevent automatic sign-up when accepting an invitation. Defaults to `false`.
**cleanupExpiredInvitations**: `boolean` - Clean up expired invitations when a value is fetched. Default `true`.
**cleanupPersonalInvitesOnDecision**: `boolean` - Cleanup personal invitations when a decision is made. Default `false`.
**verifyEmailOnAccept**: `boolean` - Whether to verify email addresses when accepting invitations. Default `true`.
**resendExistingInvite**: `boolean` - Whether to resend existing invitations, instead of creating a new one. Default `false`.
**hooks.create.before**: `(ctx: GenericEndpointContext) => Promise | void` - A function that runs before an invitation is created.
**hooks.create.after**: `(ctx: GenericEndpointContext, invitation: AppInvitation) => Promise | void` - A function that runs after an invitation was created.
**hooks.accept.before**: `(ctx: GenericEndpointContext, userToCreate: Partial & { email: string }) => Promise<{ user?: User; } | void> | { user?: User; } | void;` - A function that runs before an invitation is accepted.
**hooks.accept.after**: `(ctx: GenericEndpointContext, data: { invitation: AppInvitation; user: User; }) => Promise | void` - A function that runs after an invitation was accepted.
**hooks.reject.before**: `(ctx: GenericEndpointContext, invitation: AppInvitation) => Promise | void` - A function that runs before an invitation is rejected.
**hooks.reject.after**: `(ctx: GenericEndpointContext, invitation: AppInvitation) => Promise | void` - A function that runs after an invitation was rejected.
**hooks.cancel.before**: `(ctx: GenericEndpointContext, invitation: AppInvitation) => Promise | void` - A function that runs before an invitation is canceled.
**hooks.cancel.after**: `(ctx: GenericEndpointContext, invitation: AppInvitation) => Promise | void` - A function that runs after an invitation was canceled.
**schema**: The schema for the app-invite plugin. Allows you to infer additional fields for the `user` and `appInvitation` tables. This option is available in the client plugin as well.
~~**allowUserToCreateInvitation**~~: `boolean` | `((user: User, type: "personal" | "public") => Promise | boolean)` - A function that determines whether a user can invite others. By defaults it's `true`. You can set it to `false` to restrict users from creating invitations. (deprecated. use `canCreateInvitation` instead.)
~~**allowUserToCancelInvitation**~~: `(data: { user: User, invitation: AppInvitation }) => Promise | boolean` - A function that determines whether a user can cancel invitations. By default the user can only cancel invites created by them. You can set it to `false` to restrict users from canceling invitations. (deprecated. use `canCancelInvitation` instead.)
# plugins: Help Desk
URL: /docs/plugins/help-desk
Source: https://raw.githubusercontent.com/better-auth-extended/better-auth-extended/refs/heads/main/apps/www/content/docs/plugins/help-desk.mdx
undefined
***
## title: Help Desk
WIP (GH-16)
# plugins: Legal Consent
URL: /docs/plugins/legal-consent
Source: https://raw.githubusercontent.com/better-auth-extended/better-auth-extended/refs/heads/main/apps/www/content/docs/plugins/legal-consent.mdx
Collect and manage user legal consents efficiently for compliance purposes.
***
title: Legal Consent
description: Collect and manage user legal consents efficiently for compliance purposes.
----------------------------------------------------------------------------------------
Coming Soon
# plugins: Onboarding
URL: /docs/plugins/onboarding
Source: https://raw.githubusercontent.com/better-auth-extended/better-auth-extended/refs/heads/main/apps/www/content/docs/plugins/onboarding.mdx
Easily add onboarding to your authentication flow.
***
title: Onboarding
description: Easily add onboarding to your authentication flow.
packageName: "@better-auth-extended/onboarding"
-----------------------------------------------
The Onboarding plugin allows you to create multi-step onboarding flows for new users. It automatically tracks completion status, enforces step requirements, and integrates seamlessly with your authentication flow.
## Features
* **Multi-step onboarding flows** with custom validation
* **Automatic completion tracking** per user
* **Required step enforcement** before marking onboarding complete
* **One-time step protection** to prevent duplicate completions
* **Built-in presets** for common onboarding scenarios
* **Client-side integration** with automatic redirects
## Installation
### Install the plugin
npm
pnpm
yarn
bun
```bash
npm install @better-auth-extended/onboarding
```
```bash
pnpm add @better-auth-extended/onboarding
```
```bash
yarn add @better-auth-extended/onboarding
```
```bash
bun add @better-auth-extended/onboarding
```
### Add the plugin to your auth config
To use the Onboarding plugin, add it to your auth config.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import {
onboarding,
createOnboardingStep,
} from "@better-auth-extended/onboarding";
import { z } from "zod";
export const auth = betterAuth({
// ... other config options
plugins: [
onboarding({
steps: {
legalConsent: createOnboardingStep({
input: z.object({
tosAccepted: z.boolean(),
privacyPolicyAccepted: z.boolean(),
marketingConsent: z.boolean().optional().default(false),
}),
async handler(ctx) {
const { tosAccepted, privacyPolicyAccepted, marketingConsent } =
ctx.body;
if (!tosAccepted || !privacyPolicyAccepted) {
// Don't mark step as completed
throw ctx.error("UNAVAILABLE_FOR_LEGAL_REASONS");
}
},
required: true,
once: true,
}),
profile: createOnboardingStep({
input: z.object({
bio: z.string().optional(),
gender: z.enum(["m", "f", "d"]).optional(),
dateOfBirth: z.date().optional(),
}),
async handler(ctx) {
// Create or update user profile
const profile = await createProfile(ctx.body);
return profile;
},
}),
},
completionStep: "profile",
}),
],
});
```
### Add the client plugin
Include the client plugin in your auth client instance.
```ts
import { createAuthClient } from "better-auth/client";
import { onboardingClient } from "@better-auth-extended/onboarding/client";
import type { auth } from "./your/path"; // Import as type
const authClient = createAuthClient({
plugins: [
onboardingClient({
onOnboardingRedirect: () => {
window.location.href = "/onboarding";
},
}),
],
});
```
### Run migrations
This plugin adds additional fields to the user table. [Click here to see the schema](#schema)
npm
pnpm
yarn
bun
```bash
npx @better-auth/cli migrate
```
```bash
pnpm dlx @better-auth/cli migrate
```
```bash
yarn dlx @better-auth/cli migrate
```
```bash
bun x @better-auth/cli migrate
```
or generate
npm
pnpm
yarn
bun
```bash
npx @better-auth/cli generate
```
```bash
pnpm dlx @better-auth/cli generate
```
```bash
yarn dlx @better-auth/cli generate
```
```bash
bun x @better-auth/cli generate
```
## Usage
The Onboarding plugin provides several endpoints to manage the onboarding flow. Users can check if they need onboarding, complete steps, and verify their progress.
### Check if user needs onboarding
Use the `shouldOnboard` function to check if a user needs to complete onboarding.
### Client Side
```ts
const { data, error } = await authClient.onboarding.shouldOnboard({});
```
### Server Side
```ts
const data = await auth.api.shouldOnboard({});
```
### Type Definition
```ts
type shouldOnboard = {
}
```
### Complete onboarding step
Use the `onboardingStep` function to complete a specific onboarding step. The step name is derived from your step configuration
### Client Side
```ts
const { data, error } = await authClient.onboarding.step.profile({
bio, // required
gender, // required
dateOfBirth, // required
});
```
### Server Side
```ts
const data = await auth.api.onboardingStepProfile({
body: {
bio, // required
gender, // required
dateOfBirth, // required
}
});
```
### Type Definition
```ts
type onboardingStepProfile = {
/**
* Example property
*/
bio?: string
/**
* Example property
*/
gender?: "m" | "f" | "d"
/**
* Example property
*/
dateOfBirth?: Date
}
```
### Check step access
Use the `canAccessOnboardingStep` function to check if a user can access a specific step. This is useful for preventing access to steps that shouldn't be available.
### Client Side
```ts
const { data, error } = await authClient.onboarding.canAccessStep.legalConsent({});
```
### Server Side
```ts
const data = await auth.api.canAccessOnboardingStepLegalConsent({});
```
### Type Definition
```ts
type canAccessOnboardingStepLegalConsent = {
}
```
### Skip Onboarding Step
For optional completion steps, users can skip them if they're not required. This is useful when the completion step is optional but you still want to mark onboarding as complete.
### Client Side
```ts
const { data, error } = await authClient.onboarding.skipStep.myCompletionStep({});
```
### Server Side
```ts
const data = await auth.api.skipOnboardingStepMyCompletionStep({});
```
### Type Definition
```ts
type skipOnboardingStepMyCompletionStep = {
}
```
### Handle onboarding redirects
The plugin automatically handles onboarding redirects when users sign up or get their session. Configure the redirect behavior in the client plugin.
```ts title="auth-client.ts"
onboardingClient({
onOnboardingRedirect: () => {
// Custom redirect logic
window.location.href = "/onboarding";
},
});
```
## Defining steps
Steps are defined using the `createOnboardingStep` function. Each step requires a handler function and can include input validation and completion rules.
```ts
import { createOnboardingStep } from "@better-auth-extended/onboarding";
const step = createOnboardingStep({
async handler(ctx) {
// process step
},
// ... other configuration
});
```
### Options
* **input**: `ZodType` - Zod schema for request body validation.
* **handler**: `(ctx: GenericEndpointContext) => R | Promise` - Function that processes the step.
* **once**: `boolean` - If `true`, step can only be completed once. (default: `true`)
* **required**: `boolean` - If `true`, step must be completed before onboarding is done. (default: `false`)
* **requireHeaders**: `boolean` - If `true`, headers are required in context.
* **requireRequest**: `boolean` - If `true`, request object is required.
* **cloneRequest**: `boolean` - Clone the request object from router.
### Example
```ts title="auth.ts"
import { createOnboardingStep } from "@better-auth-extended/onboarding";
import { z } from "zod";
const preferencesStep = createOnboardingStep({
input: z.object({
theme: z.enum(["light", "dark", "system"]).optional().default("system"),
notifications: z.boolean().optional().default(false),
}),
async handler(ctx) {
const { theme, notifications } = ctx.body;
const preferences = await ctx.context.adapter.update({
model: "preferences",
where: [
{
field: "userId",
value: ctx.context.session.id,
},
],
update: {
theme,
notifications,
},
});
return preferences;
},
once: false,
});
```
## Presets
### Setup New Password
Prompt the user to enter and confirm a new password.
This this particularly useful to applications where the user was given a temporary password.
```ts title="auth.ts"
import { setupNewPasswordStep } from "@better-auth-extended/onboarding/presets";
onboarding({
steps: {
newPassword: setupNewPasswordStep({
required: true,
passwordSchema: {
minLength: 12,
maxLength: 128,
},
}),
},
completionStep: "newPassword",
});
```
### Setup 2FA
Allows the user to enable two-factor authentication.
```ts
import { setup2FAStep } from "@better-auth-extended/onboarding/presets";
onboarding({
steps: {
twoFactor: setup2FAStep({
required: true,
}),
},
completionStep: "twoFactor",
});
```
## Schema
The plugin adds additional fields to the `user` table.
Table Name: `user`
## Options
**steps**: `Record` - Object mapping step IDs to step configurations. Each step defines the input validation, handler function, and completion rules.
**completionStep**: `keyof Steps` - the step ID that marks onboarding as complete. Once this step is completed, the user's `shouldOnboard` field is set to `false`.
**autoEnableOnSignUp**: `boolean` | `(ctx: GenericEndpointContext) => Promise | boolean` - Whether to automatically enable onboarding for new users during sign up. (default: `true`)
**secondaryStorage**: `boolean` - Whether to use secondary storage instead of the database. (default: `false`)
**schema**: Custom schema configuration for renaming fields or adding additional configuration.
## Best Practises
### 1. Progressive Disclosure
Break down onboarding into logical, digestible steps:
```ts
const steps = {
welcome: createOnboardingStep({
/* welcome step */
}),
profile: createOnboardingStep({
/* basic profile */
}),
preferences: createOnboardingStep({
/* user preferences */
}),
verification: createOnboardingStep({
/* email/phone verification */
}),
};
```
### 2. Input Validation
Always validate user input with Zod schemas:
```ts
createOnboardingStep({
input: z
.object({
email: z.email("Invalid email address"),
phone: z.regex(/^\+?[\d\s-()]+$/, "Invalid phone number"),
})
.refine((data) => data.email || data.phone, {
message: "Either email or phone is required",
path: ["email"],
}),
async handler(ctx) {
// ...
},
});
```
### 3. Required vs Optional Steps
Use the `required` flag to distinguish between essential and optional steps:
```ts
const steps = {
terms: createOnboardingStep({ required: true }), // Must complete
profile: createOnboardingStep({ required: false }), // Optional
preferences: createOnboardingStep(), // Optional
};
```
### 4. One-Time vs Repeatable Steps
Use the `once` flag to distinguish between one-time and repeatable steps:
```ts
const steps = {
terms: createOnboardingStep(), // Submit only once
profile: createOnboardingStep({ once: true }), // Submit only once
preferences: createOnboardingStep({ once: false }), // Allow multiple submits
};
```
### 5. Secondary Storage
If applicable, use secondary storage instead of the main database.
```ts
onboarding({
secondaryStorage: true,
});
```
# plugins: Preferences
URL: /docs/plugins/preferences
Source: https://raw.githubusercontent.com/better-auth-extended/better-auth-extended/refs/heads/main/apps/www/content/docs/plugins/preferences.mdx
Define and manage preferences with scoped settings.
***
title: Preferences
description: Define and manage preferences with scoped settings.
packageName: "@better-auth-extended/preferences"
------------------------------------------------
The Preferences plugin allows you to define, read, and write preferences across multiple scopes (e.g., user, project, organization). It supports scoped preferences with optional `scopeId`, group operations, default values, and encryption for sensitive values.
## Installation
### Install the plugin
npm
pnpm
yarn
bun
```bash
npm install @better-auth-extended/preferences
```
```bash
pnpm add @better-auth-extended/preferences
```
```bash
yarn add @better-auth-extended/preferences
```
```bash
bun add @better-auth-extended/preferences
```
### Add the plugin to your auth config
To use the Preferences plugin, add it to your auth config.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { preferences, createPreferenceScope } from "@better-auth-extended/preferences";
import { z } from "zod";
export const auth = betterAuth({
// ... other config options
plugins: [
preferences({
scopes: {
user: createPreferenceScope({
preferences: {
theme: { type: z.enum(["light", "dark", "system"]) },
notifications: { type: z.boolean() },
},
defaultValues: {
theme: "system",
notifications: false,
},
}),
project: createPreferenceScope({
preferences: {
buildChannel: { type: z.enum(["stable", "beta"]) },
envSecrets: { type: z.record(z.string(), z.string()), sensitive: true },
},
requireScopeId: true,
groups: {
ui: {
preferences: {
buildChannel: true,
envSecrets: true
},
operations: ["read", "write"],
},
},
}),
},
}),
],
});
```
### Add the client plugin
Include the client plugin in your auth client instance.
```ts
import { createAuthClient } from "better-auth/client";
import { preferencesClient } from "@better-auth-extended/preferences/client";
const authClient = createAuthClient({
plugins: [preferencesClient()],
});
```
### Run migrations
This plugin adds an additional table to the database. [Click here to see the schema](#schema)
npm
pnpm
yarn
bun
```bash
npx @better-auth/cli migrate
```
```bash
pnpm dlx @better-auth/cli migrate
```
```bash
yarn dlx @better-auth/cli migrate
```
```bash
bun x @better-auth/cli migrate
```
or generate
npm
pnpm
yarn
bun
```bash
npx @better-auth/cli generate
```
```bash
pnpm dlx @better-auth/cli generate
```
```bash
yarn dlx @better-auth/cli generate
```
```bash
bun x @better-auth/cli generate
```
## Basic Usage
The Preferences plugin provides several endpoints to manage preferences. You can get and set individual preferences or work with groups of preferences.
### Get Preference
Retrieve a single preference value by scope and key.
### Client Side
```ts
const { data, error } = await authClient.preferences.user.theme.get({
scopeId, // required
});
```
### Server Side
```ts
const data = await auth.api.getUserThemePreference({
query: {
scopeId, // required
}
});
```
### Type Definition
```ts
type getUserThemePreference = {
/**
* The scope ID if the scope requires it
*/
scopeId?: string
}
```
or
### Client Side
```ts
const { data, error } = await authClient.preferences.getPreference({
scope,
key,
scopeId, // required
});
```
### Server Side
```ts
const data = await auth.api.getPreference({
query: {
scope,
key,
scopeId, // required
}
});
```
### Type Definition
```ts
type getPreference = {
/**
* The scope of the preference
*/
scope: string
/**
* The key of the preference
*/
key: string
/**
* The scope ID if the scope requires it
*/
scopeId?: string
}
```
### Set Preference
Set or update a single preference value.
### Client Side
```ts
const { data, error } = await authClient.preferences.user.theme.set({
scopeId, // required
value,
});
```
### Server Side
```ts
const data = await auth.api.setUserThemePreference({
body: {
scopeId, // required
value,
}
});
```
### Type Definition
```ts
type setUserThemePreference = {
/**
* The scope ID if the scope requires it
*/
scopeId?: string
/**
* The value to set
*/
value: "light" | "dark" | "system"
}
```
or
### Client Side
```ts
const { data, error } = await authClient.preferences.setPreference({
scope,
key,
value,
scopeId, // required
});
```
### Server Side
```ts
const data = await auth.api.setPreference({
body: {
scope,
key,
value,
scopeId, // required
}
});
```
### Type Definition
```ts
type setPreference = {
/**
* The scope of the preference
*/
scope: string
/**
* The key of the preference
*/
key: string
/**
* The value to set
*/
value: unknown
/**
* The scope ID if the scope requires it
*/
scopeId?: string
}
```
### Get Group
Retrieve multiple preferences at once using a defined group.
### Client Side
```ts
const { data, error } = await authClient.preferences.project.$ui.get({
scopeId, // required
});
```
### Server Side
```ts
const data = await auth.api.getProjectUiPreferences({
query: {
scopeId, // required
}
});
```
### Type Definition
```ts
type getProjectUiPreferences = {
/**
* The scope ID if the scope requires it
*/
scopeId?: string
}
```
### Set Group
Set multiple preferences at once using a defined group.
### Client Side
```ts
const { data, error } = await authClient.preferences.project.$ui.set({
scopeId, // required
values,
});
```
### Server Side
```ts
const data = await auth.api.setProjectUiPreferences({
body: {
scopeId, // required
values,
}
});
```
### Type Definition
```ts
type setProjectUiPreferences = {
/**
* The scope ID if the scope requires it
*/
scopeId?: string
/**
* The values to set
*/
values: Record
}
```
## Creating Scopes
Define your scopes with their preferences, validation types, defaults, optional groups, and access rules. Each scope and preference automatically generates the corresponding endpoints above.
```ts title="auth.ts"
import { preferences, createPreferenceScope } from "@better-auth-extended/preferences";
import { z } from "zod";
preferences({
scopes: {
user: createPreferenceScope({
preferences: {
theme: { type: z.enum(["light", "dark", "system"]) },
notifications: { type: z.boolean() },
},
defaultValues: {
theme: "system",
notifications: false,
},
}),
project: preferences.createScope({
preferences: {
buildChannel: { type: z.enum(["stable", "beta"]) },
envSecrets: { type: z.record(z.string(), z.string()), sensitive: true },
},
requireScopeId: true,
groups: {
ui: {
preferences: {
buildChannel: true,
envSecrets: true
},
operations: ["read", "write"],
},
},
}),
},
});
```
### Options
**preferences**: `Record` - Object mapping preference keys to their type definitions.
**groups**: `Record>; operations?: "read" | "write" | ("read" | "write")[]; }>` - Optional groups for batch operations.
**defaultValues**: `Partial any)>>` - Default values for preferences. Can be static values or functions.
**canRead**: `boolean | Permission` | `(data, ctx) => boolean | Permission | Promise` - Permission check for reading preferences.
**canWrite**: `boolean | Permission` | `(data, ctx) => boolean | Permission | Promise` - Permission check for writing preferences.
Returning `Permission` requires the [admin plugin](https://www.better-auth.com/docs/plugins/admin) to be configured. If it's not set up, the request will fail.
Example:
```ts title="auth.ts"
canWrite: {
statement: "preferences",
permissions: ["write"],
};
```
**requireScopeId**: `boolean` - Whether the scope requires a `scopeId` for all operations.
**disableUserBinding**: `boolean` - Whether to disable binding preferences to users.
**mergeStrategy**: `"deep"` | `"replace"` - How to merge default values with stored values.
**sensitive**: `boolean` - Whether to encrypt values at rest for the entire scope.
**secret**: `string` - Custom secret for encryption/decryption.
## Schema
Table Name: `preference`
## Options
**scopes**: `Record` - Object mapping scope names to their configuration.
# plugins: Waitlist
URL: /docs/plugins/waitlist
Source: https://raw.githubusercontent.com/better-auth-extended/better-auth-extended/refs/heads/main/apps/www/content/docs/plugins/waitlist.mdx
undefined
***
## title: Waitlist
WIP (GH-13)