# 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)