agentsclimarketplace

Better auth

Skill stonegiantstudio/skills/skills/better-auth

Better Auth integration expertise for TypeScript authentication. Triggers when implementing auth flows, configuring OAuth providers, setting up sessions, email verification, password reset, or any file importing from better-auth. Use for auth configuration, plugin setup, database adapters, session management, hooks, and security best practices.From its SKILL.md

Install
npx -y skills add stonegiantstudio/skills --skill better-auth

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

13.3 KB, as published. Nobody here has run it

Better Auth

Better Auth is a TypeScript-first, framework-agnostic authentication framework. Comprehensive, extensible, battle-tested.

Always consult better-auth.com/docs for latest API and examples.

Decision Tree

Is this a new/empty project?
├─ YES → New project setup
│   1. Identify framework (React Router, Next.js, etc.)
│   2. Choose database adapter
│   3. Install better-auth + scoped packages
│   4. Create auth.server.ts + auth.client.ts
│   5. Set up route handler
│   6. Run CLI migrate/generate
│   7. Add features via plugins
│
└─ NO → Does project have existing auth?
    ├─ YES → Migration
    │   • Audit current auth for gaps
    │   • Plan incremental migration
    │   • See migration guides: Auth0, Clerk, Auth.js, Supabase
    │
    └─ NO → Add auth to existing project
        1. Analyze project structure
        2. Install better-auth
        3. Create auth config files
        4. Add route handler
        5. Run schema migrations
        6. Integrate into existing pages

Quick Reference

Installation

Use the project's package manager (detect from lock file).

# Core package
<pkg> add better-auth

# Scoped packages (as needed)
<pkg> add @better-auth/passkey    # WebAuthn/Passkey
<pkg> add @better-auth/sso        # SAML/OIDC enterprise SSO
<pkg> add @better-auth/stripe     # Stripe payments
<pkg> add @better-auth/expo       # React Native/Expo

# Generate/apply schema (use bunx for bun, npx for others)
npx @better-auth/cli@latest migrate    # Built-in adapter
npx @better-auth/cli@latest generate   # Prisma/Drizzle

Re-run CLI after adding/changing plugins.

Environment Variables

BETTER_AUTH_SECRET=   # Min 32 chars. Generate: openssl rand -base64 32
BETTER_AUTH_URL=      # Base URL: https://example.com

Only set baseURL/secret in config if env vars are NOT set.

Core Config Options

OptionNotes
databaseRequired. pg.Pool, mysql2, sqlite, or ORM adapter
emailAndPassword{ enabled: true } to activate
socialProviders{ google: { clientId, clientSecret }, ... }
pluginsArray of plugins
sessionExpiry, cookie cache settings
secondaryStorageRedis/KV for sessions & rate limits
trustedOriginsCSRF whitelist
basePathDefault /api/auth. Set / for root

Server Config

// lib/auth.server.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  database: pool, // pg.Pool, mysql2, better-sqlite3, or ORM adapter

  emailAndPassword: { enabled: true },

  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    },
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
  },

  plugins: [
    // Import from dedicated paths for tree-shaking
    // import { twoFactor } from "better-auth/plugins/two-factor"
  ],
});

// Type inference
export type Session = typeof auth.$Infer.Session;
export type User = typeof auth.$Infer.Session.user;

Client Setup

// lib/auth.client.ts
import { createAuthClient } from "better-auth/react"; // or /client, /vue, /svelte, /solid

export const authClient = createAuthClient({
  baseURL: import.meta.env.VITE_APP_URL, // Optional if same origin
  plugins: [], // Client-side plugins must match server
});

// Type-safe client for separate projects
// createAuthClient<typeof auth>()

Route Handler by Framework

FrameworkFileHandler
React Router v7app/routes/api.auth.$.tsauth.handler(request)
Next.js App Routerapp/api/auth/[...all]/route.tstoNextJsHandler(auth)
Next.js Pagespages/api/auth/[...all].tstoNextJsHandler(auth)
SvelteKitsrc/hooks.server.tssvelteKitHandler(auth)
SolidStartRoute filesolidStartHandler(auth)
HonoRoute fileauth.handler(c.req.raw)
ExpressAny fileapp.all("/api/auth/*", toNodeHandler(auth))

React Router v7 Integration

// app/routes/api.auth.$.ts - Catch-all auth route
import { auth } from "~/lib/auth.server";
import type { Route } from "./+types/api.auth.$";

export async function loader({ request }: Route.LoaderArgs) {
  return auth.handler(request);
}

export async function action({ request }: Route.ActionArgs) {
  return auth.handler(request);
}
// app/routes/_protected.tsx - Layout with auth guard
import { redirect } from "react-router";
import { auth } from "~/lib/auth.server";
import type { Route } from "./+types/_protected";

export async function loader({ request }: Route.LoaderArgs) {
  const session = await auth.api.getSession({ headers: request.headers });
  if (!session) throw redirect("/login");
  return { user: session.user };
}

export default function ProtectedLayout({ loaderData }: Route.ComponentProps) {
  return <Outlet context={{ user: loaderData.user }} />;
}
// app/routes/login.tsx - Client-side auth
import { authClient } from "~/lib/auth.client";

export default function Login() {
  const handleGoogleLogin = () => {
    authClient.signIn.social({ provider: "google" });
  };

  const handleEmailLogin = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    await authClient.signIn.email({
      email: formData.get("email") as string,
      password: formData.get("password") as string,
    });
  };

  return (
    <div>
      <button onClick={handleGoogleLogin}>Sign in with Google</button>
      <form onSubmit={handleEmailLogin}>
        <input name="email" type="email" required />
        <input name="password" type="password" required />
        <button type="submit">Sign In</button>
      </form>
    </div>
  );
}

Database Adapters

DatabaseSetup
PostgreSQLnew Pool({ connectionString }) directly
MySQLmysql2 pool directly
SQLitebetter-sqlite3 or bun:sqlite instance
DrizzledrizzleAdapter(db, { provider: "pg" })
PrismaprismaAdapter(prisma, { provider: "postgresql" })
MongoDBmongodbAdapter(client.db("mydb"))

Critical: Use ORM model names, NOT table names. If Prisma model is User mapping to users table, use modelName: "user".

Session Management

Storage priority:

  1. If secondaryStorage defined → sessions go there (not DB)
  2. Set session.storeSessionInDatabase: true to also persist to DB
  3. No database + cookieCache → fully stateless mode
session: {
  expiresIn: 60 * 60 * 24 * 7, // 7 days (seconds)
  updateAge: 60 * 60 * 24,     // Refresh after 1 day

  cookieCache: {
    enabled: true,
    strategy: "compact", // "compact" | "jwt" | "jwe"
    maxAge: 60 * 5,      // 5 minutes
    version: 1,          // Change to invalidate all sessions
  },

  storeSessionInDatabase: true, // Required if using secondaryStorage
}

Cookie cache strategies:

  • compact (default) - Base64url + HMAC. Smallest size.
  • jwt - Standard JWT. Readable but signed.
  • jwe - Encrypted. Maximum security.

Email Configuration

emailAndPassword: {
  enabled: true,
  requireEmailVerification: true,
  sendResetPassword: async ({ user, url }) => {
    await sendEmail({
      to: user.email,
      subject: "Reset your password",
      html: `<a href="${url}">Reset password</a>`,
    });
  },
},

emailVerification: {
  sendOnSignUp: true,
  sendVerificationEmail: async ({ user, url }) => {
    await sendEmail({
      to: user.email,
      subject: "Verify your email",
      html: `<a href="${url}">Verify email</a>`,
    });
  },
},

Hooks

// Endpoint hooks (before/after HTTP handlers)
hooks: {
  before: [
    {
      matcher: (ctx) => ctx.path.startsWith("/api/auth"),
      handler: createAuthMiddleware(async (ctx) => {
        console.log("Auth request:", ctx.path);
        return { context: ctx.context }; // Continue
      }),
    },
  ],
  after: [
    {
      matcher: (ctx) => ctx.path === "/api/auth/sign-up",
      handler: createAuthMiddleware(async (ctx) => {
        if (ctx.context.returned) {
          await trackSignUp(ctx.context.returned.user);
        }
      }),
    },
  ],
},

// Database hooks
databaseHooks: {
  user: {
    create: {
      before: async (user) => {
        return { data: { ...user, role: "member" } };
      },
      after: async (user) => {
        await createDefaultWorkspace(user.id);
      },
    },
  },
},

Hook context (ctx.context): session, secret, authCookies, password.hash()/verify(), adapter, internalAdapter, generateId(), tables, baseURL.

Plugins

Import from dedicated paths for tree-shaking:

import { twoFactor } from "better-auth/plugins/two-factor";
// NOT: import { twoFactor } from "better-auth/plugins"
PluginServer ImportClient ImportPurpose
twoFactorbetter-auth/plugins/two-factortwoFactorClient2FA with TOTP/OTP
organizationbetter-auth/plugins/organizationorganizationClientTeams/orgs
adminbetter-auth/plugins/adminadminClientUser management
passkey@better-auth/passkeypasskeyClientWebAuthn
magicLinkbetter-auth/plugins/magic-linkmagicLinkClientPasswordless
bearerbetter-auth/plugins/bearer-API token auth
jwtbetter-auth/plugins/jwt-JWT tokens
sso@better-auth/sso-Enterprise SSO
openAPIbetter-auth/plugins/open-api-API docs

Plugin pattern: Server plugin + client plugin + run migrations.

Security Options

advanced: {
  useSecureCookies: true,        // Force HTTPS cookies
  crossSubDomainCookies: {
    enabled: true,
    domain: ".example.com",
  },
  ipAddress: {
    ipAddressHeaders: ["x-forwarded-for", "x-real-ip"],
  },
  database: {
    generateId: "uuid",          // "uuid" | "serial" | false | () => string
  },
},

rateLimit: {
  enabled: true,
  window: 60,                    // Seconds
  max: 10,                       // Requests per window
  storage: "secondary-storage",  // "memory" | "database" | "secondary-storage"
},

trustedOrigins: ["https://app.example.com"],

Security Checklist

  • BETTER_AUTH_SECRET set (32+ chars, generated securely)
  • advanced.useSecureCookies: true in production
  • trustedOrigins configured for all valid domains
  • Rate limits enabled (rateLimit.enabled: true)
  • Email verification enabled for email/password auth
  • Password reset flow implemented with sendResetPassword
  • 2FA enabled for sensitive applications
  • CSRF protection NOT disabled (disableCSRFCheck is false)
  • account.accountLinking settings reviewed
  • OAuth redirect URIs verified in provider dashboards

Client Methods Reference

// Sign up/in
authClient.signUp.email({ email, password, name });
authClient.signIn.email({ email, password });
authClient.signIn.social({ provider: "google", callbackURL: "/dashboard" });

// Session
const session = authClient.useSession(); // React hook: { data, isPending }
const session = await authClient.getSession();

// Sign out
await authClient.signOut();

// Session management
await authClient.revokeSession({ sessionId });
await authClient.revokeSessions(); // All other sessions

Troubleshooting

IssueCauseFix
"Secret not set"Missing env varAdd BETTER_AUTH_SECRET to environment
"Invalid Origin"CSRF protectionAdd domain to trustedOrigins array
Sessions not persistingsecondaryStorage definedSessions go to Redis by default. Set storeSessionInDatabase: true
Cookies not settingDomain mismatchCheck baseURL matches domain; enable secure cookies in prod
Custom fields missingCookie cache limitCustom session fields always re-fetch from DB
Plugin schema missingCLI not re-runRun npx @better-auth/cli migrate after adding plugins
Model not foundTable vs model nameUse ORM model name, not DB table name
Email verification failingNo handlerMust define sendVerificationEmail function
Logout not workingStateless modeNo DB = session in cookie only, clears on cache expiry
OAuth callback errorsRedirect URIVerify redirect URIs in provider dashboard
Type errors after pluginSchema outdatedRe-run CLI generate/migrate
Change email confusingTwo-step flowSends to current email first, then new email

Resources

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.