Skip to content
Back to skills

Better Auth Best Practices Hasparus

ASecurity

A skill to integrate better-auth. And auth framework for TypeScript projects.

  • 57 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 17, 2026
databasestypescriptrustgosqlreactvuenextjsnodeexpresstesting

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 17, 2026

npx -y skills add diegosouzapw/awesome-omni-skill --skill better-auth-best-practices-hasparus --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Better Auth Best Practices Hasparus?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Better Auth Best Practices Hasparus
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/diegosouzapw-better-auth-best-practices-hasparus/badge)](https://www.skillsdirectory.com/skills/diegosouzapw-better-auth-best-practices-hasparus)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: Better Auth Best Practices
description: A skill to integrate better-auth. And auth framework for TypeScript projects.
---

Best practices for building Better Auth integrations. Use when implementing authentication, authorization, session management, OAuth providers, or any Better Auth integration.

When designing an authentication integration, always prefer [Better Auth's Documentation](https://better-auth.com/docs).

You should always default to the latest version of Better Auth unless the user specifies otherwise.

Better Auth is a TypeScript-first authentication framework that provides a secure, flexible, and developer-friendly approach to authentication. It supports multiple methods including email/password, OAuth providers, magic links, and passwordless authentication.

## Core Principles

### 1. Packaging

Most imports come from the npm package `better-auth` (initializer), `better-auth/plugins` (plugins), and `better-auth/adapters/*` (adapters). Some plugins use a scoped package like `@better-auth/stripe`.

### 2. Environment Variables

Better Auth requires two environment variables:
- `BETTER_AUTH_SECRET`: A high-entropy secret used for encryption and hashing (at least 32 characters).
- `BETTER_AUTH_URL`: The base URL for the server where Better Auth is mounted.

### 3. Auth Config

Initialize Better Auth by calling `betterAuth` with a config object. Export it as `const auth` or as a default export unless you are wrapping the initializer (for example, to pass runtime context in edge environments). The CLI looks for `auth.ts` in `./`, `./lib`, `./utils`, or those directories under `src`.

### 4. Database

The database connection is the most important part of the auth config. If no database is provided, Better Auth uses the in-memory adapter (non-persistent). Built-in adapters support database clients like `pg`, `mysql2`, `better-sqlite3`, and `bun:sqlite`. You can also use adapters like `drizzleAdapter`, `prismaAdapter`, and `mongodbAdapter`.

### 5. CLI

Use the Better Auth CLI to manage schema:
- `npx @better-auth/cli@latest migrate` applies schema directly for the built-in Kysely adapter.
- `npx @better-auth/cli@latest generate` generates schema for Prisma/Drizzle (apply with your ORM).
- The CLI supports `--config` to point to a non-default `auth.ts` location.

### 6. Authentication Methods

Configure methods in the auth config. Core methods include `emailAndPassword` and `socialProviders`. Enable email/password with `emailAndPassword: { enabled: true }`. Social providers are defined under `socialProviders`. For additional OAuth integrations, use the `genericOAuth` plugin. Other plugins include `username`, `phoneNumber`, `emailOtp`, `magicLink`, `passkey`, and `anonymous`. Plugins can add schema requirements, so re-run CLI generate/migrate when you add or change plugins.

### 7. Handler

All HTTP requests are handled by `auth.handler`. Mount it according to the framework-specific integration docs.

### 8. Client

Better Auth provides clients to interact with the auth server. Client files are typically named `auth-client.ts`.

React example client:

```ts
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
    /** The base URL of the server (optional if you're using the same domain) */
    baseURL: "http://localhost:3000"
})
```

Other than React, there are clients for vanilla JS, Vue, Svelte, and Solid. See https://www.better-auth.com/docs/concepts/client

### 9. Type Safety

Better Auth is built with TypeScript and provides full type safety. Infer types with `auth.$Infer`.

### 10. Session

Sessions are stored in the database unless you provide `secondaryStorage`, which can be used for session data or rate limiting in high-performance stores.

### 11. Auth Options

Review https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts for the full configuration surface. Key skill areas include:

- **Rate limiting**: Configure `rateLimit` defaults, per-path `customRules`, and storage (`memory`, `database`, `secondary-storage`, or `customStorage`).
- **Advanced security**: Use `advanced.useSecureCookies`, and be cautious with `advanced.disableCSRFCheck` or `advanced.disableOriginCheck` (these reduce protections).
- **Trusted origins**: Prefer dynamic `trustedOrigins` when running behind proxies or multiple hostnames.
- **Cookie strategy**: Customize cookie names/attributes, prefixes, and cross-subdomain settings via `advanced.cookies`, `advanced.cookiePrefix`, and `advanced.crossSubDomainCookies`.
- **Session tuning**: Adjust `session.expiresIn`, `session.updateAge`, `session.cookieCache`, and `session.freshAge` based on UX/security needs.
- **Account linking**: Configure `account.accountLinking` carefully, especially `allowDifferentEmails` and `allowUnlinkingAll`.
- **Email flows**: Use `emailVerification` hooks and `emailAndPassword.sendResetPassword` to implement secure verification and recovery.
- **Data model mapping**: Map fields and add metadata via `user.fields`, `session.fields`, `account.fields`, and `verification.fields`.
- **Database hooks**: Use `databaseHooks` to enforce policies or auditing on user/session/account/verification lifecycle events.
- **Error handling**: Centralize response behavior with `onAPIError` and customize the default error page if needed.
- **Telemetry**: Control data collection with `telemetry.enabled` and `telemetry.debug`.
- **Experimental flags**: Keep `experimental.joins` off unless the adapter supports it and the docs recommend it.

---

## Plugin Architecture

Plugins follow a consistent structure with these key properties:

| Property | Description |
|----------|-------------|
| `id` | Unique string identifier |
| `endpoints` | Object of `createAuthEndpoint` calls that define API routes |
| `schema` | Database schema additions the plugin needs (triggers migrations) |
| `hooks.before` / `hooks.after` | Middleware that runs before/after matched endpoints |
| `init` | Called during auth initialization to modify context or options |
| `onRequest` / `onResponse` | Request/response interceptors |
| `$ERROR_CODES` | Plugin-specific error code constants |
| `rateLimit` | Custom rate limit rules for plugin endpoints |

When building custom plugins, use `createAuthEndpoint` and `createAuthMiddleware` from `@better-auth/core/api`. Use `sessionMiddleware` from `better-auth/api` to require authentication.

### Example Plugin Structure

```ts
import type { BetterAuthPlugin } from "@better-auth/core";
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
import { sessionMiddleware } from "better-auth/api";
import { z } from "zod";

export const myPlugin = (options?: MyPluginOptions) => {
  return {
    id: "my-plugin",
    endpoints: {
      myEndpoint: createAuthEndpoint(
        "/my-plugin/action",
        {
          method: "POST",
          body: z.object({ data: z.string() }),
          use: [sessionMiddleware], // Require authentication
        },
        async (ctx) => {
          const user = ctx.context.session.user;
          // Implementation
          return { success: true };
        }
      ),
    },
    schema: {
      myTable: {
        fields: {
          userId: { type: "string", references: { model: "user", field: "id" } },
          data: { type: "string" },
        },
      },
    },
    $ERROR_CODES: {
      MY_ERROR: "My custom error message",
    },
  } satisfies BetterAuthPlugin;
};
```

---

## Client-Server Type Inference

Use `auth.$Infer` on the server and client plugins expose `$InferServerPlugin` for type bridging:

```ts
// Server plugin exports
export const myPlugin = () => ({
  id: "my-plugin",
  $ERROR_CODES: MY_ERROR_CODES,
  // ...
}) satisfies BetterAuthPlugin;

// Client plugin
export const myPluginClient = () => ({
  $InferServerPlugin: {} as ReturnType<typeof myPlugin>,
  getActions: ($fetch) => ({
    myAction: async () => {
      return $fetch("/my-plugin/action", { method: "POST" });
    },
  }),
}) satisfies BetterAuthClientPlugin;
```

---

## Social Providers

### Built-in Providers

`apple`, `atlassian`, `cognito`, `discord`, `dropbox`, `facebook`, `figma`, `github`, `gitlab`, `google`, `huggingface`, `kakao`, `kick`, `line`, `linear`, `linkedin`, `microsoft`, `naver`, `notion`, `paybin`, `paypal`, `polar`, `reddit`, `roblox`, `salesforce`, `slack`, `spotify`, `tiktok`, `twitch`, `twitter`, `vercel`, `vk`, `zoom`

### Generic OAuth

For providers not in this list, use the `genericOAuth` plugin with pre-built configurations for Auth0, Gumroad, HubSpot, Keycloak, Line, Microsoft Entra ID, Okta, Patreon, and Slack.

```ts
import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";

const auth = betterAuth({
  // Built-in providers
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
  },
  // Generic OAuth for other providers
  plugins: [
    genericOAuth({
      config: [
        {
          providerId: "custom-provider",
          clientId: process.env.CUSTOM_CLIENT_ID!,
          clientSecret: process.env.CUSTOM_CLIENT_SECRET!,
          authorizationUrl: "https://provider.com/oauth/authorize",
          tokenUrl: "https://provider.com/oauth/token",
          userInfoUrl: "https://provider.com/api/user",
        },
      ],
    }),
  ],
});
```

---

## Scoped Packages

Some plugins live in separate packages:

| Package | Description |
|---------|-------------|
| `@better-auth/stripe` | Stripe subscriptions and payments |
| `@better-auth/expo` | Expo/React Native client |
| `@better-auth/passkey` | Passkey/WebAuthn authentication |
| `@better-auth/sso` | SAML/OIDC enterprise SSO |
| `@better-auth/scim` | SCIM user provisioning |

---

## Framework Integrations

### Next.js

```ts
import { toNextJsHandler } from "better-auth/integrations/next-js";
import { auth } from "./auth";

export const { GET, POST } = toNextJsHandler(auth);
```

For Server Components cookie handling, add `nextCookies()` plugin:

```ts
import { nextCookies } from "better-auth/integrations/next-js";

const auth = betterAuth({
  plugins: [nextCookies()],
});
```

### Other Frameworks

- **SvelteKit**: Use `svelteKitHandler()`
- **SolidStart**: Use `solidStartHandler()`
- **TanStack Start**: Use `tanstackStartHandler()`
- **Node/Express**: Mount `auth.handler` directly as middleware

---

## Testing

For unit tests, use `getTestInstance()` from `better-auth/test-utils`:

```ts
import { getTestInstance } from "better-auth/test-utils";
import { describe, it, expect } from "vitest";

describe("my auth tests", () => {
  it("should authenticate user", async () => {
    const { client, signInWithTestUser } = await getTestInstance({
      plugins: [myPlugin()],
    });

    const { headers, user } = await signInWithTestUser();
    
    const result = await client.myPlugin.action({
      fetchOptions: { headers },
    });
    
    expect(result.data).toBeDefined();
  });
});
```

### What `getTestInstance()` Provides

- In-memory SQLite database by default (or postgres/mysql/mongodb via config)
- Automatic migration execution
- Pre-configured client with custom fetch
- Test user (`test@test.com` / `test123456`)
- `signInWithTestUser()` for authenticated test flows
- Automatic cleanup via `afterAll`

---

## Account Linking

Configure `account.accountLinking` carefully:

| Option | Description |
|--------|-------------|
| `enabled` | Toggle account linking (default: true) |
| `trustedProviders` | Array of provider IDs that can link without email verification |
| `allowDifferentEmails` | Allow linking accounts with different emails (security risk) |
| `allowUnlinkingAll` | Allow users to unlink all OAuth accounts |

Accounts are automatically linked when a user signs in with a different provider using the same verified email.

```ts
const auth = betterAuth({
  account: {
    accountLinking: {
      enabled: true,
      trustedProviders: ["google", "github"], // These can link without email verification
      allowDifferentEmails: false, // Keep false for security
    },
  },
});
```

---

## Session Cookie Strategies

`session.cookieCache.strategy` options:

| Strategy | Description |
|----------|-------------|
| `"compact"` | (default) Stores minimal session data in cookies |
| `"full"` | Stores complete session/user data |
| `"jwt"` | Uses signed JWT for stateless verification (requires `jwt` plugin) |

```ts
const auth = betterAuth({
  session: {
    cookieCache: {
      enabled: true,
      strategy: "compact",
      maxAge: 60 * 5, // 5 minutes
    },
  },
});
```

---

## Internal Adapter

Access database operations via `ctx.context.internalAdapter` in endpoints:

- `findUserByEmail()`, `findUserById()`
- `createUser()`, `updateUser()`, `deleteUser()`
- `createSession()`, `findSession()`, `deleteSession()`
- `linkAccount()`, `findOAuthUser()`
- `createVerification()`, `findVerification()`

These methods automatically apply `databaseHooks` (before/after lifecycle events).

---

## Database Hooks

Use `databaseHooks` for lifecycle events on core models:

```ts
const auth = betterAuth({
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          // Modify user before creation
          return { data: { ...user, customField: "value" } };
        },
        after: async (user, ctx) => {
          // Audit log, send welcome email, etc.
          await sendWelcomeEmail(user.email);
        },
      },
    },
    session: {
      create: {
        after: async (session, ctx) => {
          // Track session creation
        },
      },
    },
  },
});
```

---

## Error Handling

Use `APIError` from `better-call` for throwing errors:

```ts
import { APIError } from "better-call";
import { BASE_ERROR_CODES } from "@better-auth/core/error";

// In an endpoint
throw new APIError("BAD_REQUEST", { 
  message: BASE_ERROR_CODES.INVALID_PASSWORD 
});

// Custom error codes in plugins
export const MY_ERROR_CODES = {
  CUSTOM_ERROR: "Custom error occurred",
  VALIDATION_FAILED: "Validation failed",
} as const;
```

Error codes should be exported as `$ERROR_CODES` in plugins. Reference `BASE_ERROR_CODES` from `@better-auth/core/error` for standard codes.

---

## Background Tasks

For edge runtimes (Vercel, Cloudflare Workers), configure `advanced.backgroundTasks.handler` with the platform's `waitUntil` function:

```ts
// Vercel
import { waitUntil } from "@vercel/functions";

const auth = betterAuth({
  advanced: {
    backgroundTasks: {
      handler: waitUntil,
    },
  },
});

// Cloudflare Workers (with AsyncLocalStorage)
const auth = betterAuth({
  advanced: {
    backgroundTasks: {
      handler: (p) => execCtxStorage.getStore()?.waitUntil(p),
    },
  },
});
```

---

## Secondary Storage

Use `secondaryStorage` for high-performance session/rate-limit storage:

```ts
import { betterAuth } from "better-auth";
import { Redis } from "ioredis";

const redis = new Redis();

const auth = betterAuth({
  secondaryStorage: {
    get: async (key) => {
      const value = await redis.get(key);
      return value ? JSON.parse(value) : null;
    },
    set: async (key, value, ttl) => {
      if (ttl) {
        await redis.setex(key, ttl, JSON.stringify(value));
      } else {
        await redis.set(key, JSON.stringify(value));
      }
    },
    delete: async (key) => {
      await redis.del(key);
    },
  },
});
```

---

## IP Address Handling

Configure IP address extraction for rate limiting and session tracking:

```ts
const auth = betterAuth({
  advanced: {
    ipAddress: {
      ipAddressHeaders: ["x-real-ip", "x-forwarded-for", "cf-connecting-ip"],
      disableIpTracking: false, // Only disable if absolutely necessary
    },
  },
});
```

---

## Resources

- [Better Auth Documentation](https://better-auth.com/docs)
- [GitHub Repository](https://github.com/better-auth/better-auth)
- [Examples](https://better-auth.com/docs/examples)
- [Init Options Reference](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts)

Always consult the official documentation for the most up-to-date best practices and API changes.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.