Skip to main content
This guide walks through creating a complete Better Auth plugin from scratch. We’ll build a birthday plugin that stores user birth dates and enforces an age requirement during sign-up.
This guide assumes you have already set up Better Auth in your project.

How plugins work

A Better Auth plugin is a pair of objects:
  • Server plugin — registered in betterAuth({ plugins: [] }). Defines the schema, endpoints, and lifecycle hooks.
  • Client plugin — registered in createAuthClient({ plugins: [] }). Mirrors the server types and can add client-side helper methods.
Both implement a simple interface (BetterAuthPlugin / BetterAuthClientPlugin) that Better Auth merges at startup.

Building the server plugin

1

Create the plugin file

Create a birthday-plugin/index.ts file:
birthday-plugin/index.ts
import type { BetterAuthPlugin } from "better-auth";

export const birthdayPlugin = () =>
  ({
    id: "birthdayPlugin",
  } satisfies BetterAuthPlugin);
This is already a valid plugin — it just doesn’t do anything yet.
2

Define the schema

Extend the built-in user model with a birthday field. Better Auth’s CLI reads this schema when running generate or migrate.
birthday-plugin/index.ts
import type { BetterAuthPlugin } from "better-auth";

export const birthdayPlugin = () =>
  ({
    id: "birthdayPlugin",
    schema: {
      user: {
        fields: {
          birthday: {
            type: "date",     // "string" | "number" | "boolean" | "date"
            required: true,
            unique: false,
          },
        },
      },
    },
  } satisfies BetterAuthPlugin);
3

Add a before hook

Use a before hook to validate that the signing-up user is at least 5 years old. Hooks receive the request context and can throw APIError to reject the request.
birthday-plugin/index.ts
import type { BetterAuthPlugin } from "better-auth";
import { createAuthMiddleware, APIError } from "better-auth/api";

export const birthdayPlugin = () =>
  ({
    id: "birthdayPlugin",
    schema: {
      user: {
        fields: {
          birthday: { type: "date", required: true },
        },
      },
    },
    hooks: {
      before: [
        {
          matcher: (ctx) => ctx.path.startsWith("/sign-up/email"),
          handler: createAuthMiddleware(async (ctx) => {
            const { birthday } = ctx.body;

            if (!(birthday instanceof Date)) {
              throw new APIError("BAD_REQUEST", {
                message: "Birthday must be a Date object.",
              });
            }

            const fiveYearsAgo = new Date();
            fiveYearsAgo.setFullYear(fiveYearsAgo.getFullYear() - 5);

            if (birthday >= fiveYearsAgo) {
              throw new APIError("BAD_REQUEST", {
                message: "User must be older than 5 years.",
              });
            }

            return { context: ctx };
          }),
        },
      ],
    },
  } satisfies BetterAuthPlugin);
Return { context: ctx } at the end of a before hook to pass the (potentially modified) context to the next handler. Return nothing to short-circuit with an implicit 200.
4

Register the server plugin

Add the plugin to your auth config:
auth.ts
import { betterAuth } from "better-auth";
import { birthdayPlugin } from "./birthday-plugin";

export const auth = betterAuth({
  plugins: [birthdayPlugin()],
});

Building the client plugin

1

Create the client file

Create birthday-plugin/client.ts. The client plugin’s main job is to import the server plugin’s return type so that client types stay in sync.
birthday-plugin/client.ts
import type { BetterAuthClientPlugin } from "better-auth/client";
import type { birthdayPlugin } from "./index";

type BirthdayPlugin = typeof birthdayPlugin;

export const birthdayClientPlugin = () =>
  ({
    id: "birthdayPlugin",
    $InferServerPlugin: {} as ReturnType<BirthdayPlugin>,
  } satisfies BetterAuthClientPlugin);
2

Register the client plugin

auth-client.ts
import { createAuthClient } from "better-auth/client";
import { birthdayClientPlugin } from "./birthday-plugin/client";

export const authClient = createAuthClient({
  plugins: [birthdayClientPlugin()],
});

Update the database schema

After writing your plugin, generate or apply the schema changes:
# For Prisma / Drizzle — generate the schema file
npx auth@latest generate

# For Kysely — apply migrations directly
npx auth@latest migrate

Plugin structure reference

A server plugin can implement any combination of these properties:
PropertyTypePurpose
idstringUnique plugin identifier
schemaSchemaDatabase table extensions
hooks.beforeHook[]Middleware run before handlers
hooks.afterHook[]Middleware run after handlers
endpointsRecord<string, Endpoint>New API endpoints
middlewaresMiddleware[]Express-style middleware
init(ctx) => voidCalled once at startup
onRequest(request, ctx) => voidCalled on every request
onResponse(response, ctx) => voidCalled on every response
See the plugins documentation for the complete API.

Next steps

  • Read the plugins concepts page for advanced patterns like adding custom endpoints and custom client methods.
  • Share your plugin on Discord or open a PR to share it with the community.