AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified Apache-2.0 Self-run

Better Auth

skill-stonegiantstudio-skills-better-auth · by stonegiantstudio

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.

No reviews yet
0 installs
29 views
0.0% view→install

Install

$ agentstack add skill-stonegiantstudio-skills-better-auth

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-stonegiantstudio-skills-better-auth)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Better Auth? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

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
 add better-auth

# Scoped packages (as needed)
 add @better-auth/passkey    # WebAuthn/Passkey
 add @better-auth/sso        # SAML/OIDC enterprise SSO
 add @better-auth/stripe     # Stripe payments
 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

| Option | Notes | |--------|-------| | database | Required. pg.Pool, mysql2, sqlite, or ORM adapter | | emailAndPassword | { enabled: true } to activate | | socialProviders | { google: { clientId, clientSecret }, ... } | | plugins | Array of plugins | | session | Expiry, cookie cache settings | | secondaryStorage | Redis/KV for sessions & rate limits | | trustedOrigins | CSRF whitelist | | basePath | Default /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()

Route Handler by Framework

| Framework | File | Handler | |-----------|------|---------| | React Router v7 | app/routes/api.auth.$.ts | auth.handler(request) | | Next.js App Router | app/api/auth/[...all]/route.ts | toNextJsHandler(auth) | | Next.js Pages | pages/api/auth/[...all].ts | toNextJsHandler(auth) | | SvelteKit | src/hooks.server.ts | svelteKitHandler(auth) | | SolidStart | Route file | solidStartHandler(auth) | | Hono | Route file | auth.handler(c.req.raw) | | Express | Any file | app.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 ;
}
// 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) => {
    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 (
    
      Sign in with Google
      
        
        
        Sign In
      
    
  );
}

Database Adapters

| Database | Setup | |----------|-------| | PostgreSQL | new Pool({ connectionString }) directly | | MySQL | mysql2 pool directly | | SQLite | better-sqlite3 or bun:sqlite instance | | Drizzle | drizzleAdapter(db, { provider: "pg" }) | | Prisma | prismaAdapter(prisma, { provider: "postgresql" }) | | MongoDB | mongodbAdapter(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: `Reset password`,
    });
  },
},

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

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"

| Plugin | Server Import | Client Import | Purpose | |--------|---------------|---------------|---------| | twoFactor | better-auth/plugins/two-factor | twoFactorClient | 2FA with TOTP/OTP | | organization | better-auth/plugins/organization | organizationClient | Teams/orgs | | admin | better-auth/plugins/admin | adminClient | User management | | passkey | @better-auth/passkey | passkeyClient | WebAuthn | | magicLink | better-auth/plugins/magic-link | magicLinkClient | Passwordless | | bearer | better-auth/plugins/bearer | - | API token auth | | jwt | better-auth/plugins/jwt | - | JWT tokens | | sso | @better-auth/sso | - | Enterprise SSO | | openAPI | better-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

| Issue | Cause | Fix | |-------|-------|-----| | "Secret not set" | Missing env var | Add BETTER_AUTH_SECRET to environment | | "Invalid Origin" | CSRF protection | Add domain to trustedOrigins array | | Sessions not persisting | secondaryStorage defined | Sessions go to Redis by default. Set storeSessionInDatabase: true | | Cookies not setting | Domain mismatch | Check baseURL matches domain; enable secure cookies in prod | | Custom fields missing | Cookie cache limit | Custom session fields always re-fetch from DB | | Plugin schema missing | CLI not re-run | Run npx @better-auth/cli migrate after adding plugins | | Model not found | Table vs model name | Use ORM model name, not DB table name | | Email verification failing | No handler | Must define sendVerificationEmail function | | Logout not working | Stateless mode | No DB = session in cookie only, clears on cache expiry | | OAuth callback errors | Redirect URI | Verify redirect URIs in provider dashboard | | Type errors after plugin | Schema outdated | Re-run CLI generate/migrate | | Change email confusing | Two-step flow | Sends to current email first, then new email |

Resources

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.