Install
$ agentstack add skill-stonegiantstudio-skills-better-auth ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
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 →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:
- If
secondaryStoragedefined → sessions go there (not DB) - Set
session.storeSessionInDatabase: trueto also persist to DB - 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_SECRETset (32+ chars, generated securely) - [ ]
advanced.useSecureCookies: truein production - [ ]
trustedOriginsconfigured 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 (
disableCSRFCheckis false) - [ ]
account.accountLinkingsettings 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
- Documentation
- Options Reference
- GitHub
- LLMs.txt
- Examples
- Migration Guides (Auth0, Clerk, Auth.js, Supabase)
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: stonegiantstudio
- Source: stonegiantstudio/skills
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.