# 插件 (/docs/concepts/plugins)

了解如何使用和创建 Better Auth 插件，包括定义端点、模式、钩子、中间件、限速规则、受信任来源，以及使用自定义操作和 Atom 构建客户端插件。



插件是 Better Auth 的关键部分，它们允许您扩展基础功能。您可以使用插件添加新的身份验证方法、功能，或者自定义行为。

Better Auth 自带许多内置插件，随时可用。请查看插件章节了解详情。您也可以创建自己的插件。

## 使用插件 [#使用插件]

插件可以是服务器端插件、客户端插件，或者两者兼有。

要在服务器端添加插件，将其包含在认证配置的 `plugins` 数组中。插件将使用提供的选项进行初始化。

```ts title="server.ts"
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    plugins: [
        // 在此添加您的插件
    ]
});
```

客户端插件在创建客户端时添加。大多数插件需要服务器端和客户端插件共同配合才能正常工作。
前端的 Better Auth 认证客户端使用 `better-auth/client` 提供的 `createAuthClient` 函数。

```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";

const authClient = createAuthClient({
    plugins: [
        // 在此添加您的客户端插件
    ]
});
```

我们建议将认证客户端和您的常规认证实例保存在单独的文件中。

<Files>
  <Folder name="auth">
    <File name="server.ts" />

    <File name="auth-client.ts" />
  </Folder>
</Files>

## 创建插件 [#创建插件]

首先，您需要一个服务器插件。\
服务器插件是所有插件的核心，客户端插件提供与前端 API 的接口，便于与服务器插件协作。

<Callout type="info">
  如果您的服务器插件有需要从客户端调用的端点，您还需要创建客户端插件。
</Callout>

### 插件能做什么？ [#插件能做什么]

* 创建自定义的 `endpoint` 执行任意操作。
* 使用自定义的 `schemas` 扩展数据库表。
* 使用 `middleware` 针对一组路由，利用路由匹配器，仅在通过请求调用这些路由时运行。
* 使用 `hooks` 针对特定路由或请求。如果您希望即使直接调用端点也能执行钩子。
* 如果想做影响所有请求或响应的事情，可以使用 `onRequest` 或 `onResponse`。
* 创建自定义的 `rate-limit` 规则。

## 创建服务器插件 [#创建服务器插件]

创建服务器插件时，需要传入满足 `BetterAuthPlugin` 接口的对象。

唯一必填属性是 `id`，它是插件唯一标识符。
服务器端与客户端插件可以使用相同的 `id`。

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";

export const myPlugin = () => {
    return {
        id: "my-plugin",
    } satisfies BetterAuthPlugin
}
```

<Callout>
  您不必将插件定义为函数，但建议这样做。这样，您可以向插件传递选项，并且与内置插件保持一致。
</Callout>

### 注册插件类型 [#注册插件类型]

可复用插件可以扩展 `BetterAuthPluginRegistry`，使 `getPlugin()` 返回其确切类型。注册表键必须与插件的 `id` 相同：

```ts title="plugin.ts"
declare module "@better-auth/core" {
  interface BetterAuthPluginRegistry<AuthOptions, Options> {
    "my-plugin": {
      creator: typeof myPlugin;
    };
  }
}
```

这只影响 TypeScript。用户仍然必须在运行时将插件添加到 `plugins` 数组中。

<Callout type="info">
  如果您将插件作为独立包发布，请将 `@better-auth/core` 声明为对等依赖项，并确保其支持的版本范围与 `better-auth` 相同。
</Callout>

### 端点 [#端点]

要向服务器添加端点，可以传入 `endpoints`，它是一个对象，键是任意字符串，值为 `AuthEndpoint`。

创建 Auth 端点需要从 `better-auth` 导入 `createAuthEndpoint`。

Better Auth 封装了另一个名为 <Link href="https://github.com/bekacru/better-call"> Better Call </Link> 的库来创建端点。Better Call 是一个简单的 TypeScript Web 框架，由与 Better Auth 相同的团队打造。

```ts title="plugin.ts"
import { createAuthEndpoint } from "better-auth/api";
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = () => {
    return {
        id: "my-plugin",
        endpoints: {
            getHelloWorld: createAuthEndpoint("/my-plugin/hello-world", {
                method: "GET",
            }, async(ctx) => {
                return ctx.json({
                    message: "Hello World"
                })
            })
        }
    } satisfies BetterAuthPlugin
}
```

创建 Auth 端点实际上封装了 Better Call 的 `createEndpoint`。在 `ctx` 对象内部，还会提供一个名为 `context` 的对象，供您访问 Better Auth 特定的上下文，包括 `options`、`db`、`baseURL` 等。

**Context 对象**

* `appName`：应用程序的名称。默认为 "Better Auth"。
* `options`：传递给 Better Auth 实例的选项。
* `tables`：核心表定义。它是一个对象，以表名为键，模式定义为值。
* `baseURL`：认证服务器的 baseURL。这包括路径。例如，如果服务器运行在 `http://localhost:3000`，那么 baseURL 将是 `http://localhost:3000/api/auth`，除非用户更改。
* `session`：会话配置。包括 `updateAge` 和 `expiresIn` 值。
* `secret`：用于各种目的的秘密密钥。这由用户定义。
* `authCookie`：核心认证 cookie 的默认配置。
* `logger`：Better Auth 使用的日志记录器实例。
* `db`：Better Auth 用于与数据库交互的 Kysely 实例。
* `adapter`：这与 db 相同，但它提供了类似 ORM 的函数来与数据库交互。（我们建议使用这个而不是 `db`，除非您需要原始 SQL 查询或出于性能原因）。
* `internalAdapter`：这些是 Better Auth 内部使用的数据库调用。例如，您可以使用这些调用来创建会话，而不是直接使用 `adapter`。`internalAdapter.createSession(userId)`
* `createAuthCookie`：这是一个帮助函数，让您获取用于 `set` 或 `get` cookie 的 cookie `name` 和 `options`。它实现了基于连接是否安全（HTTPS）或应用程序是否运行在生产模式下的 `__Secure-` 前缀等功能。
* `trustedOrigins`：这是您通过 `options.trustedOrigins` 指定的受信任来源列表。
* `isTrustedOrigin`：这是一个帮助函数，允许您快速检查给定的 URL 或路径是否基于受信任来源配置受信任。

要查看其他属性，请参考 <Link href="https://github.com/bekacru/better-call">Better Call</Link> 文档和 <Link href="https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/context/create-context.ts">源代码 </Link>。

**端点规则**

* 确保端点路径使用 kebab-case
* 确保端点仅使用 `POST` 或 `GET` 方法。
* 任何修改数据的函数都应使用 `POST` 方法。
* 任何获取数据的函数都应使用 `GET` 方法。
* 确保使用 `createAuthEndpoint` 函数创建 API 端点。
* 确保路径具有唯一性，以避免与其他插件发生冲突。如果使用常见路径，请将插件名称作为路径前缀。（使用 `/my-plugin/hello-world`，而不是 `/hello-world`。）

### 模式 [#模式]

您可以通过传入 `schema` 对象定义插件的数据库模式。`schema` 对象以表名为键，模式定义为值。

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = () => {
    return {
        id: "my-plugin",
        schema: {
            myTable: {
                fields: {
                    name: {
                        type: "string"
                    }
                },
                modelName: "myTable" // 如果需要与键名不同，可以指定，否则可以省略
            }
        }
    } satisfies BetterAuthPlugin
}
```

**字段**

默认 Better Auth 会为每张表创建一个 `id` 字段。您可以通过向 `fields` 对象添加字段，自定义额外列。

字段名作为键，字段定义为值。定义属性包括：

* `type`：字段类型，可选 `string`、`number`、`boolean`、`date`
* `required`：是否为必填字段，（默认 `true`）
* `unique`：字段是否唯一，（默认 `false`）
* `references`：字段是否为关联另一张表的外键（可选），对象格式包含：
  * `model`：所关联表名
  * `field`：所关联字段名
  * `onDelete`：关联记录删除时操作（默认 `cascade`）

**其他 Schema 属性**

`unique`：字段是否必须唯一。（默认：`false`）

对于跨多个字段的索引，请向表中添加 `indexes` 数组。即使字段具有自定义数据库列名，索引字段也使用 `fields` 中的逻辑字段键。

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";

const directoryPlugin = {
  id: "directory",
  schema: {
    directoryUser: {
      fields: {
        connectionId: {
          type: "string",
          fieldName: "connection_id",
        },
        externalId: {
          type: "string",
          fieldName: "external_id",
        },
      },
      indexes: [
        {
          fields: ["connectionId", "externalId"],
          unique: true,
        },
      ],
    },
  },
} satisfies BetterAuthPlugin;
```

每个索引最多可以包含 16 个字段。当每个字段元组最多只能标识一行时，将 `unique: true` 设置为唯一索引。唯一的表级索引中的每个字段都必须是必填字段，以确保跨数据库的 null 处理保持一致。您还可以提供一个模式范围的 `name`，长度最多为 63 个 UTF-8 字节，以控制数据库索引名称。名称必须以字母或下划线开头，且只能包含字母、数字和下划线。名称比较不区分大小写，不能与表或其他索引冲突。CLI 会将表级索引包含在 SQL 迁移以及生成的 Drizzle 或 Prisma 模式中。MongoDB 适配器会在对集合执行首次索引写入前创建这些索引。

`references`：如果字段是对另一张表的引用。（可选）它接受一个包含以下属性的对象：

* `model`：要引用的表名。
* `field`：要引用的字段名。
* `onDelete`：所引用记录被删除时要执行的操作。（默认：`cascade`）

**其他 Schema 属性**

`disableMigration`：表是否不应进行迁移。（默认：`false`）

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = (opts: PluginOptions) => {
    return {
        id: "my-plugin",
        schema: {
            rateLimit: {
                fields: {
                    key: {
                        type: "string",
                    },
                },
                disableMigration: opts.storage.provider !== "database", // [!code highlight]
            },
        },
    } satisfies BetterAuthPlugin
}
```

如果您在 `user` 或 `session` 表添加额外字段，`getSession` 和 `signUpEmail` 调用时类型会自动推断。

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = () => {
    return {
        id: "my-plugin",
        schema: {
            user: {
                fields: {
                    age: {
                        type: "number",
                    },
                },
            },
        },
    } satisfies BetterAuthPlugin
}
```

这样会为 `user` 表添加 `age` 字段，所有返回 `user` 的接口也会包含此字段，且 TypeScript 会正确推断。

<Callout type="warn">
  不要在 `user` 或 `session` 表中存储敏感信息。如需存储敏感信息，请创建新表。
</Callout>

### 钩子（Hooks） [#钩子hooks]

钩子用于在操作执行前后运行代码，无论是从客户端还是直接在服务器上执行。您可以通过传入 `hooks` 对象添加钩子，该对象须包含 `before` 和 `after` 属性。

```ts title="plugin.ts"
import { createAuthMiddleware } from "better-auth/api";

const myPlugin = () => {
    return {
        id: "my-plugin",
        hooks: {
            before: [{
                    matcher: (context) => {
                        return context.headers.get("x-my-header") === "my-value"
                    },
                    handler: createAuthMiddleware(async (ctx) => {
                        // 在请求前执行一些操作
                        return  {
                            context: ctx // 如需修改上下文
                        }
                    })
                }],
            after: [{
                matcher: (context) => {
                    return context.path === "/sign-up/email"
                },
                handler: createAuthMiddleware(async (ctx) => {
                    return ctx.json({
                        message: "Hello World"
                    }) // 如需修改响应
                })
            }]
        }
    } satisfies BetterAuthPlugin
}
```

### 中间件 [#中间件]

您可以通过传入 `middlewares` 数组给服务器添加中间件。数组中每项为中间件对象，包含 `path` 和 `middleware` 属性。与钩子不同，中间件仅在客户端发起的 API 请求时执行，若直接调用端点，中间件不会运行。

`path` 可以为字符串或路径匹配器，使用与 `better-call` 相同的路径匹配系统。

中间件内若抛出 `APIError` 或返回 `Response` 对象，请求将停止，响应发送给客户端。

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";

const myPlugin = () => {
    return {
        id: "my-plugin",
        middlewares: [
            {
                path: "/my-plugin/hello-world",
                middleware: createAuthMiddleware(async(ctx) => {
                    // 执行操作
                })
            }
        ]
    } satisfies BetterAuthPlugin
}
```

### 请求时与响应时 [#请求时与响应时]

#### 请求前 [#请求前]

`onRequest` 函数在请求发起前执行，接受两个参数：`request` 和 `context`。

用法示例：

以下是其工作方式：

* **正常继续**：如果您不返回任何内容，请求将按正常流程继续。
* **中断请求**：要停止请求并发送响应，请返回一个包含 `response` 属性的对象，该属性包含一个 `Response` 对象。
* **修改请求**：您还可以返回一个修改后的 `request` 对象，以在请求发送前更改请求。

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = () => {
    return  {
        id: "my-plugin",
        onRequest: async (request, context) => {
            // 执行操作
        },
    } satisfies BetterAuthPlugin
}
```

#### 响应后 [#响应后]

`onResponse` 函数在响应返回后立即执行，接受两个参数：`response` 和 `context`。

用法示例：

* **修改响应**：您可以返回一个修改后的响应对象以更改响应，然后再发送给客户端。
* **正常继续**：如果您不返回任何内容，响应将按原样发送。

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = () => {
    return {
        id: "my-plugin",
        onResponse: async (response, context) => {
            // 执行操作
        },
    } satisfies BetterAuthPlugin
}
```

### 限速规则 [#限速规则]

您可以通过传入 `rateLimit` 数组定义自定义限速规则。数组项为限速对象。

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = () => {
    return {
        id: "my-plugin",
        rateLimit: [
            {
                pathMatcher: (path) => {
                    return path === "/my-plugin/hello-world"
                },
                limit: 10,
                window: 60,
            }
        ]
    } satisfies BetterAuthPlugin
}
```

### 受信任来源 [#受信任来源]

如果您正在构建自定义插件或端点，可以使用认证上下文中的 `isTrustedOrigin()` 方法根据受信任来源配置验证 URL。这样确保自定义端点遵循与 Better Auth 内置端点相同的安全设置。

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";
import { createAuthEndpoint, APIError } from "better-auth/api";
import * as z from "zod"

const myPlugin = () => {
    return {
        id: "my-plugin",
        trustedOrigins: [
            "http://trusted.com"
        ],
        endpoints: {
            getTrustedHelloWorld: createAuthEndpoint("/my-plugin/hello-world", {
                method: "GET",
                query: z.object({
                    url: z.string()
                }),
            }, async (ctx) => {
                // allowRelativePaths 选项用于允许或禁止相对路径
                if (!ctx.context.isTrustedOrigin(ctx.query.url, { allowRelativePaths: false })) {
                    throw new APIError("FORBIDDEN", {
                        message: "origin is not trusted."
                    });
                }

                return ctx.json({
                    message: "Hello World"
                })
            })
        }
    } satisfies BetterAuthPlugin
}
```

更多信息请参见[受信任来源和安全](/docs/reference/security#受信任来源)文档。

### 服务器插件辅助函数 [#服务器插件辅助函数]

下面是一些创建服务器插件的辅助函数。

#### `getSessionFromCtx` [#getsessionfromctx]

通过传入认证中间件的 `context`，可获取客户端会话数据。

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";
import { createAuthMiddleware, getSessionFromCtx } from "better-auth/api";

const myPlugin = {
    id: "my-plugin",
    hooks: {
        before: [{
                matcher: (context) => {
                    return context.headers.get("x-my-header") === "my-value"
                },
                handler: createAuthMiddleware(async (ctx) => {
                    const session = await getSessionFromCtx(ctx);
                    // 操作客户端的会话数据

                    return  {
                        context: ctx
                    }
                })
            }],
    }
} satisfies BetterAuthPlugin
```

#### `sessionMiddleware` [#sessionmiddleware]

此中间件用于检查客户端是否具有有效会话，若有效，则会将会话数据添加到上下文对象中。

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";
import { createAuthEndpoint, sessionMiddleware } from "better-auth/api";

const myPlugin = () => {
    return {
        id: "my-plugin",
        endpoints: {
            getHelloWorld: createAuthEndpoint("/my-plugin/hello-world", {
                method: "GET",
                use: [sessionMiddleware], // [!code highlight]
            }, async (ctx) => {
                const session = ctx.context.session;
                return ctx.json({
                    message: "Hello World"
                })
            })
        }
    } satisfies BetterAuthPlugin
}
```

#### `requireResourceOwnership` [#requireresourceownership]

一个中间件，用于通过 ID 加载资源并验证其属于认证用户。在 `sessionMiddleware` 之后用于操作用户拥有的模型的端点。

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";
import * as z from "zod";
import {
    createAuthEndpoint,
    requireResourceOwnership,
    sessionMiddleware,
} from "better-auth/api";

const myPlugin = () => {
    return {
        id: "my-plugin",
        endpoints: {
            deleteApiKey: createAuthEndpoint("/my-plugin/delete-api-key", {
                method: "POST",
                body: z.object({
                    id: z.string(),
                }),
                use: [
                    sessionMiddleware,
                    requireResourceOwnership({
                        model: "apiKey",
                        idParam: "id",
                        idSource: "body",
                    }),
                ],
            }, async (ctx) => {
                return ctx.json({ success: true });
            })
        }
    } satisfies BetterAuthPlugin
}
```

如果您的模型在不同字段名下存储所有者，请使用 `ownerField`，如果要返回插件特定的错误代码，请使用 `notFoundError` 或 `forbiddenError`。

#### `requireOrgRole` [#requireorgrole]

一个中间件，用于验证认证用户是否是特定组织的成员，并且（可选）具有一组允许的角色之一。在组织范围的端点中，在 `sessionMiddleware` 之后使用。

```ts title="plugin.ts"
import type { BetterAuthPlugin } from "better-auth";
import * as z from "zod";
import {
    createAuthEndpoint,
    requireOrgRole,
    sessionMiddleware,
} from "better-auth/api";

const myPlugin = () => {
    return {
        id: "my-plugin",
        endpoints: {
            syncDirectory: createAuthEndpoint("/my-plugin/sync-directory", {
                method: "POST",
                body: z.object({
                    organizationId: z.string(),
                }),
                use: [
                    sessionMiddleware,
                    requireOrgRole({
                        orgIdParam: "organizationId",
                        orgIdSource: "body",
                        allowedRoles: ["admin", "owner"],
                    }),
                ],
            }, async (ctx) => {
                return ctx.json({ success: true });
            })
        }
    } satisfies BetterAuthPlugin
}
```

如果省略 `allowedRoles`，则接受任何组织成员。具有多个角色的成员在任一角色匹配时即被授权。

## 创建客户端插件 [#创建客户端插件]

如果您的端点需要从客户端调用，您还需要创建一个客户端插件。Better Auth 客户端可以从服务器插件推断端点。您也可以添加额外的客户端逻辑。

```ts title="client-plugin.ts"
import type { BetterAuthClientPlugin } from "better-auth/client";

export const myPluginClient = () => {
    return {
        id: "my-plugin",
    } satisfies BetterAuthClientPlugin
}
```

### 端点接口 [#端点接口]

客户端插件添加 `$InferServerPlugin` 字段，用于推断服务器端插件的端点。

客户端将路径作为对象，并将 kebab-case 转换为 camelCase。例如，`/my-plugin/hello-world` 转为 `myPlugin.helloWorld`。

```ts title="client-plugin.ts"
import type { BetterAuthClientPlugin } from "better-auth/client";
import type { myPlugin } from "./plugin";

const myPluginClient = () => {
    return  {
        id: "my-plugin",
        $InferServerPlugin: {} as ReturnType<typeof myPlugin>,
    } satisfies BetterAuthClientPlugin
}
```

### 获取 actions [#获取-actions]

如需为客户端添加额外方法，可使用 `getActions` 函数，它接收客户端的 `fetch` 函数。

Better Auth 使用 <Link href="https://better-fetch.vercel.app">Better Fetch</Link> 进行请求，它是作者开发的简单 fetch 包装器。

```ts title="client-plugin.ts"
import type { BetterAuthClientPlugin } from "better-auth/client";
import type { myPlugin } from "./plugin";
import type { BetterFetchOption } from "@better-fetch/fetch";

const myPluginClient = {
    id: "my-plugin",
    $InferServerPlugin: {} as ReturnType<typeof myPlugin>,
    getActions: ($fetch) => {
        return {
            myCustomAction: async (data: {
                foo: string,
            }, fetchOptions?: BetterFetchOption) => {
                const res = $fetch("/custom/action", {
                    method: "POST",
                    body: {
                        foo: data.foo
                    },
                    ...fetchOptions
                })
                return res
            }
        }
    }
} satisfies BetterAuthClientPlugin
```

<Callout>
  一般指导原则是确保每个函数只接受一个参数，并使用第二个可选的 `fetchOptions` 参数，以便用户可以向 fetch 调用传递额外选项。该函数应返回一个包含 `data` 和 `error` 键的对象。

  如果您的用例涉及超出 API 调用的操作，请随意偏离此规则。
</Callout>

### 获取 Atom [#获取-atom]

此功能仅在您想提供如 `useSession` 这类钩子时有用。

`getAtoms` 函数接收 Better Fetch 的 `fetch`，返回包含各个 Atom 的对象。Atom 应使用 <Link href="https://github.com/nanostores/nanostores">nanostores</Link> 创建。Atom 会被各框架基于 nanostores 的 `useStore` 钩子自动解析。

```ts title="client-plugin.ts"
import { atom } from "nanostores";
import type { BetterAuthClientPlugin } from "better-auth/client";

const myPluginClient = {
    id: "my-plugin",
    $InferServerPlugin: {} as ReturnType<typeof myPlugin>,
    getAtoms: ($fetch) => {
        const myAtom = atom<null>()
        return {
            myAtom
        }
    }
} satisfies BetterAuthClientPlugin
```

请参阅内置插件了解如何正确使用 atoms。

### 路径方法 [#路径方法]

默认情况下，推断的路径如果不需要请求体则使用 `GET`，否则使用 `POST`。您可以通过传入 `pathMethods` 对象覆盖，键为路径，值为方法（`"POST"` | `"GET"`）。

```ts title="client-plugin.ts"
import type { BetterAuthClientPlugin } from "better-auth/client";
import type { myPlugin } from "./plugin";

const myPluginClient = {
    id: "my-plugin",
    $InferServerPlugin: {} as ReturnType<typeof myPlugin>,
    pathMethods: {
        "/my-plugin/hello-world": "POST"
    }
} satisfies BetterAuthClientPlugin
```

### Fetch 插件 [#fetch-插件]

如需使用 Better Fetch 插件，可以传入 `fetchPlugins` 数组。更多信息请参阅 <Link href="https://better-fetch.vercel.app/docs/plugins">Better Fetch 文档</Link>。

### Atom 监听器 [#atom-监听器]

此功能只有在您想提供如 `useSession` 的钩子，并且希望监听 atoms 并在改变时重新计算时才有用。

内置插件中有示例展示了此用法。

