Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:48:52 +08:00
commit 6ec3196ecc
434 changed files with 125248 additions and 0 deletions

204
skills/better-auth/SKILL.md Normal file
View File

@@ -0,0 +1,204 @@
---
name: better-auth
description: Implement authentication and authorization with Better Auth - a framework-agnostic TypeScript authentication framework. Features include email/password authentication with verification, OAuth providers (Google, GitHub, Discord, etc.), two-factor authentication (TOTP, SMS), passkeys/WebAuthn support, session management, role-based access control (RBAC), rate limiting, and database adapters. Use when adding authentication to applications, implementing OAuth flows, setting up 2FA/MFA, managing user sessions, configuring authorization rules, or building secure authentication systems for web applications.
license: MIT
version: 2.0.0
---
# Better Auth Skill
Better Auth is comprehensive, framework-agnostic authentication/authorization framework for TypeScript with built-in email/password, social OAuth, and powerful plugin ecosystem for advanced features.
## When to Use
- Implementing auth in TypeScript/JavaScript applications
- Adding email/password or social OAuth authentication
- Setting up 2FA, passkeys, magic links, advanced auth features
- Building multi-tenant apps with organization support
- Managing sessions and user lifecycle
- Working with any framework (Next.js, Nuxt, SvelteKit, Remix, Astro, Hono, Express, etc.)
## Quick Start
### Installation
```bash
npm install better-auth
# or pnpm/yarn/bun add better-auth
```
### Environment Setup
Create `.env`:
```env
BETTER_AUTH_SECRET=<generated-secret-32-chars-min>
BETTER_AUTH_URL=http://localhost:3000
```
### Basic Server Setup
Create `auth.ts` (root, lib/, utils/, or under src/app/server/):
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: {
// See references/database-integration.md
},
emailAndPassword: {
enabled: true,
autoSignIn: true
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}
}
});
```
### Database Schema
```bash
npx @better-auth/cli generate # Generate schema/migrations
npx @better-auth/cli migrate # Apply migrations (Kysely only)
```
### Mount API Handler
**Next.js App Router:**
```ts
// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
```
**Other frameworks:** See references/email-password-auth.md#framework-setup
### Client Setup
Create `auth-client.ts`:
```ts
import { createAuthClient } from "better-auth/client";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || "http://localhost:3000"
});
```
### Basic Usage
```ts
// Sign up
await authClient.signUp.email({
email: "user@example.com",
password: "secure123",
name: "John Doe"
});
// Sign in
await authClient.signIn.email({
email: "user@example.com",
password: "secure123"
});
// OAuth
await authClient.signIn.social({ provider: "github" });
// Session
const { data: session } = authClient.useSession(); // React/Vue/Svelte
const { data: session } = await authClient.getSession(); // Vanilla JS
```
## Feature Selection Matrix
| Feature | Plugin Required | Use Case | Reference |
|---------|----------------|----------|-----------|
| Email/Password | No (built-in) | Basic auth | [email-password-auth.md](./references/email-password-auth.md) |
| OAuth (GitHub, Google, etc.) | No (built-in) | Social login | [oauth-providers.md](./references/oauth-providers.md) |
| Email Verification | No (built-in) | Verify email addresses | [email-password-auth.md](./references/email-password-auth.md#email-verification) |
| Password Reset | No (built-in) | Forgot password flow | [email-password-auth.md](./references/email-password-auth.md#password-reset) |
| Two-Factor Auth (2FA/TOTP) | Yes (`twoFactor`) | Enhanced security | [advanced-features.md](./references/advanced-features.md#two-factor-authentication) |
| Passkeys/WebAuthn | Yes (`passkey`) | Passwordless auth | [advanced-features.md](./references/advanced-features.md#passkeys-webauthn) |
| Magic Link | Yes (`magicLink`) | Email-based login | [advanced-features.md](./references/advanced-features.md#magic-link) |
| Username Auth | Yes (`username`) | Username login | [email-password-auth.md](./references/email-password-auth.md#username-authentication) |
| Organizations/Multi-tenant | Yes (`organization`) | Team/org features | [advanced-features.md](./references/advanced-features.md#organizations) |
| Rate Limiting | No (built-in) | Prevent abuse | [advanced-features.md](./references/advanced-features.md#rate-limiting) |
| Session Management | No (built-in) | User sessions | [advanced-features.md](./references/advanced-features.md#session-management) |
## Auth Method Selection Guide
**Choose Email/Password when:**
- Building standard web app with traditional auth
- Need full control over user credentials
- Targeting users who prefer email-based accounts
**Choose OAuth when:**
- Want quick signup with minimal friction
- Users already have social accounts
- Need access to social profile data
**Choose Passkeys when:**
- Want passwordless experience
- Targeting modern browsers/devices
- Security is top priority
**Choose Magic Link when:**
- Want passwordless without WebAuthn complexity
- Targeting email-first users
- Need temporary access links
**Combine Multiple Methods when:**
- Want flexibility for different user preferences
- Building enterprise apps with various auth requirements
- Need progressive enhancement (start simple, add more options)
## Core Architecture
Better Auth uses client-server architecture:
1. **Server** (`better-auth`): Handles auth logic, database ops, API routes
2. **Client** (`better-auth/client`): Provides hooks/methods for frontend
3. **Plugins**: Extend both server/client functionality
## Implementation Checklist
- [ ] Install `better-auth` package
- [ ] Set environment variables (SECRET, URL)
- [ ] Create auth server instance with database config
- [ ] Run schema migration (`npx @better-auth/cli generate`)
- [ ] Mount API handler in framework
- [ ] Create client instance
- [ ] Implement sign-up/sign-in UI
- [ ] Add session management to components
- [ ] Set up protected routes/middleware
- [ ] Add plugins as needed (regenerate schema after)
- [ ] Test complete auth flow
- [ ] Configure email sending (verification/reset)
- [ ] Enable rate limiting for production
- [ ] Set up error handling
## Reference Documentation
### Core Authentication
- [Email/Password Authentication](./references/email-password-auth.md) - Email/password setup, verification, password reset, username auth
- [OAuth Providers](./references/oauth-providers.md) - Social login setup, provider configuration, token management
- [Database Integration](./references/database-integration.md) - Database adapters, schema setup, migrations
### Advanced Features
- [Advanced Features](./references/advanced-features.md) - 2FA/MFA, passkeys, magic links, organizations, rate limiting, session management
## Scripts
- `scripts/better_auth_init.py` - Initialize Better Auth configuration with interactive setup
## Resources
- Docs: https://www.better-auth.com/docs
- GitHub: https://github.com/better-auth/better-auth
- Plugins: https://www.better-auth.com/docs/plugins
- Examples: https://www.better-auth.com/docs/examples

View File

@@ -0,0 +1,553 @@
# Advanced Features
Better Auth plugins extend functionality beyond basic authentication.
## Two-Factor Authentication
### Server Setup
```ts
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
twoFactor({
issuer: "YourAppName", // TOTP issuer name
otpOptions: {
period: 30, // OTP validity period (seconds)
digits: 6, // OTP length
}
})
]
});
```
### Client Setup
```ts
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
twoFactorClient({
twoFactorPage: "/two-factor", // Redirect to 2FA verification page
redirect: true // Auto-redirect if 2FA required
})
]
});
```
### Enable 2FA for User
```ts
// Enable TOTP
const { data } = await authClient.twoFactor.enable({
password: "userPassword" // Verify user identity
});
// data contains QR code URI for authenticator app
const qrCodeUri = data.totpURI;
const backupCodes = data.backupCodes; // Save these securely
```
### Verify TOTP Code
```ts
await authClient.twoFactor.verifyTOTP({
code: "123456",
trustDevice: true // Skip 2FA on this device for 30 days
});
```
### Disable 2FA
```ts
await authClient.twoFactor.disable({
password: "userPassword"
});
```
### Backup Codes
```ts
// Generate new backup codes
const { data } = await authClient.twoFactor.generateBackupCodes({
password: "userPassword"
});
// Use backup code instead of TOTP
await authClient.twoFactor.verifyBackupCode({
code: "backup-code-123"
});
```
## Passkeys (WebAuthn)
### Server Setup
```ts
import { betterAuth } from "better-auth";
import { passkey } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
passkey({
rpName: "YourApp", // Relying Party name
rpID: "yourdomain.com" // Your domain
})
]
});
```
### Client Setup
```ts
import { createAuthClient } from "better-auth/client";
import { passkeyClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [passkeyClient()]
});
```
### Register Passkey
```ts
// User must be authenticated first
await authClient.passkey.register({
name: "My Laptop" // Optional: name for this passkey
});
```
### Sign In with Passkey
```ts
await authClient.passkey.signIn();
```
### List User Passkeys
```ts
const { data } = await authClient.passkey.list();
// data contains array of registered passkeys
```
### Delete Passkey
```ts
await authClient.passkey.delete({
id: "passkey-id"
});
```
## Magic Link
### Server Setup
```ts
import { betterAuth } from "better-auth";
import { magicLink } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
magicLink({
sendMagicLink: async ({ email, url, token }) => {
await sendEmail({
to: email,
subject: "Sign in to YourApp",
html: `Click <a href="${url}">here</a> to sign in.`
});
},
expiresIn: 300, // Link expires in 5 minutes (seconds)
})
]
});
```
### Client Setup
```ts
import { createAuthClient } from "better-auth/client";
import { magicLinkClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [magicLinkClient()]
});
```
### Send Magic Link
```ts
await authClient.magicLink.sendMagicLink({
email: "user@example.com",
callbackURL: "/dashboard"
});
```
### Verify Magic Link
```ts
// Called automatically when user clicks link
// Token in URL query params handled by Better Auth
await authClient.magicLink.verify({
token: "token-from-url"
});
```
## Organizations (Multi-Tenancy)
### Server Setup
```ts
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
allowUserToCreateOrganization: true,
organizationLimit: 5, // Max orgs per user
creatorRole: "owner" // Role for org creator
})
]
});
```
### Client Setup
```ts
import { createAuthClient } from "better-auth/client";
import { organizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [organizationClient()]
});
```
### Create Organization
```ts
await authClient.organization.create({
name: "Acme Corp",
slug: "acme", // Unique slug
metadata: {
industry: "Technology"
}
});
```
### Invite Members
```ts
await authClient.organization.inviteMember({
organizationId: "org-id",
email: "user@example.com",
role: "member", // owner, admin, member
message: "Join our team!" // Optional
});
```
### Accept Invitation
```ts
await authClient.organization.acceptInvitation({
invitationId: "invitation-id"
});
```
### List Organizations
```ts
const { data } = await authClient.organization.list();
// Returns user's organizations
```
### Update Member Role
```ts
await authClient.organization.updateMemberRole({
organizationId: "org-id",
userId: "user-id",
role: "admin"
});
```
### Remove Member
```ts
await authClient.organization.removeMember({
organizationId: "org-id",
userId: "user-id"
});
```
### Delete Organization
```ts
await authClient.organization.delete({
organizationId: "org-id"
});
```
## Session Management
### Configure Session Expiration
```ts
export const auth = betterAuth({
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days (seconds)
updateAge: 60 * 60 * 24, // Update session every 24 hours
cookieCache: {
enabled: true,
maxAge: 5 * 60 // Cache for 5 minutes
}
}
});
```
### Server-Side Session
```ts
// Next.js
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
const session = await auth.api.getSession({
headers: await headers()
});
if (!session) {
// Not authenticated
}
```
### Client-Side Session
```tsx
// React
import { authClient } from "@/lib/auth-client";
function UserProfile() {
const { data: session, isPending, error } = authClient.useSession();
if (isPending) return <div>Loading...</div>;
if (error) return <div>Error</div>;
if (!session) return <div>Not logged in</div>;
return <div>Hello, {session.user.name}!</div>;
}
```
### List Active Sessions
```ts
const { data: sessions } = await authClient.listSessions();
// Returns all active sessions for current user
```
### Revoke Session
```ts
await authClient.revokeSession({
sessionId: "session-id"
});
```
### Revoke All Sessions
```ts
await authClient.revokeAllSessions();
```
## Rate Limiting
### Server Configuration
```ts
export const auth = betterAuth({
rateLimit: {
enabled: true,
window: 60, // Time window in seconds
max: 10, // Max requests per window
storage: "memory", // "memory" or "database"
customRules: {
"/api/auth/sign-in": {
window: 60,
max: 5 // Stricter limit for sign-in
},
"/api/auth/sign-up": {
window: 3600,
max: 3 // 3 signups per hour
}
}
}
});
```
### Custom Rate Limiter
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
rateLimit: {
enabled: true,
customLimiter: async ({ request, limit }) => {
// Custom rate limiting logic
const ip = request.headers.get("x-forwarded-for");
const key = `ratelimit:${ip}`;
// Use Redis, etc.
const count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, limit.window);
}
if (count > limit.max) {
throw new Error("Rate limit exceeded");
}
}
}
});
```
## Anonymous Sessions
Track users before they sign up.
### Server Setup
```ts
import { betterAuth } from "better-auth";
import { anonymous } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [anonymous()]
});
```
### Client Usage
```ts
// Create anonymous session
const { data } = await authClient.signIn.anonymous();
// Convert to full account
await authClient.signUp.email({
email: "user@example.com",
password: "password123",
linkAnonymousSession: true // Link anonymous data
});
```
## Email OTP
One-time password via email (passwordless).
### Server Setup
```ts
import { betterAuth } from "better-auth";
import { emailOTP } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
emailOTP({
sendVerificationOTP: async ({ email, otp }) => {
await sendEmail({
to: email,
subject: "Your verification code",
text: `Your code is: ${otp}`
});
},
expiresIn: 300, // 5 minutes
length: 6 // OTP length
})
]
});
```
### Client Usage
```ts
// Send OTP to email
await authClient.emailOTP.sendOTP({
email: "user@example.com"
});
// Verify OTP
await authClient.emailOTP.verifyOTP({
email: "user@example.com",
otp: "123456"
});
```
## Phone Number Authentication
Requires phone number plugin.
### Server Setup
```ts
import { betterAuth } from "better-auth";
import { phoneNumber } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
phoneNumber({
sendOTP: async ({ phoneNumber, otp }) => {
// Use Twilio, AWS SNS, etc.
await sendSMS(phoneNumber, `Your code: ${otp}`);
}
})
]
});
```
### Client Usage
```ts
// Sign up with phone
await authClient.signUp.phoneNumber({
phoneNumber: "+1234567890",
password: "password123"
});
// Send OTP
await authClient.phoneNumber.sendOTP({
phoneNumber: "+1234567890"
});
// Verify OTP
await authClient.phoneNumber.verifyOTP({
phoneNumber: "+1234567890",
otp: "123456"
});
```
## Best Practices
1. **2FA**: Offer 2FA as optional, make mandatory for admin users
2. **Passkeys**: Implement as progressive enhancement (fallback to password)
3. **Magic Links**: Set short expiration (5-15 minutes)
4. **Organizations**: Implement RBAC for org permissions
5. **Sessions**: Use short expiration for sensitive apps
6. **Rate Limiting**: Enable in production, adjust limits based on usage
7. **Anonymous Sessions**: Clean up old anonymous sessions periodically
8. **Backup Codes**: Force users to save backup codes before enabling 2FA
9. **Multi-Device**: Allow users to manage trusted devices
10. **Audit Logs**: Track sensitive operations (role changes, 2FA changes)
## Regenerate Schema After Plugins
After adding any plugin:
```bash
npx @better-auth/cli generate
npx @better-auth/cli migrate # if using Kysely
```
Or manually apply migrations for your ORM (Drizzle, Prisma).

View File

@@ -0,0 +1,577 @@
# Database Integration
Better Auth supports multiple databases and ORMs for flexible data persistence.
## Supported Databases
- SQLite
- PostgreSQL
- MySQL/MariaDB
- MongoDB
- Any database with adapter support
## Direct Database Connection
### SQLite
```ts
import { betterAuth } from "better-auth";
import Database from "better-sqlite3";
export const auth = betterAuth({
database: new Database("./sqlite.db"),
// or
database: new Database(":memory:") // In-memory for testing
});
```
### PostgreSQL
```ts
import { betterAuth } from "better-auth";
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
// or explicit config
host: "localhost",
port: 5432,
user: "postgres",
password: "password",
database: "myapp"
});
export const auth = betterAuth({
database: pool
});
```
### MySQL
```ts
import { betterAuth } from "better-auth";
import { createPool } from "mysql2/promise";
const pool = createPool({
host: "localhost",
user: "root",
password: "password",
database: "myapp",
waitForConnections: true,
connectionLimit: 10
});
export const auth = betterAuth({
database: pool
});
```
## ORM Adapters
### Drizzle ORM
**Install:**
```bash
npm install drizzle-orm better-auth
```
**Setup:**
```ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
const db = drizzle(pool);
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg", // "pg" | "mysql" | "sqlite"
schema: {
// Optional: custom table names
user: "users",
session: "sessions",
account: "accounts",
verification: "verifications"
}
})
});
```
**Generate Schema:**
```bash
npx @better-auth/cli generate --adapter drizzle
```
### Prisma
**Install:**
```bash
npm install @prisma/client better-auth
```
**Setup:**
```ts
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "postgresql", // "postgresql" | "mysql" | "sqlite"
})
});
```
**Generate Schema:**
```bash
npx @better-auth/cli generate --adapter prisma
```
**Apply to Prisma:**
```bash
# Add generated schema to schema.prisma
npx prisma migrate dev --name init
npx prisma generate
```
### Kysely
**Install:**
```bash
npm install kysely better-auth
```
**Setup:**
```ts
import { betterAuth } from "better-auth";
import { kyselyAdapter } from "better-auth/adapters/kysely";
import { Kysely, PostgresDialect } from "kysely";
import { Pool } from "pg";
const db = new Kysely({
dialect: new PostgresDialect({
pool: new Pool({
connectionString: process.env.DATABASE_URL
})
})
});
export const auth = betterAuth({
database: kyselyAdapter(db, {
provider: "pg"
})
});
```
**Auto-migrate with Kysely:**
```bash
npx @better-auth/cli migrate --adapter kysely
```
### MongoDB
**Install:**
```bash
npm install mongodb better-auth
```
**Setup:**
```ts
import { betterAuth } from "better-auth";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import { MongoClient } from "mongodb";
const client = new MongoClient(process.env.MONGODB_URI!);
await client.connect();
export const auth = betterAuth({
database: mongodbAdapter(client, {
databaseName: "myapp"
})
});
```
**Generate Collections:**
```bash
npx @better-auth/cli generate --adapter mongodb
```
## Core Database Schema
Better Auth requires these core tables/collections:
### User Table
```sql
CREATE TABLE user (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
emailVerified BOOLEAN DEFAULT FALSE,
name TEXT,
image TEXT,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
### Session Table
```sql
CREATE TABLE session (
id TEXT PRIMARY KEY,
userId TEXT NOT NULL,
expiresAt TIMESTAMP NOT NULL,
ipAddress TEXT,
userAgent TEXT,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (userId) REFERENCES user(id) ON DELETE CASCADE
);
```
### Account Table
```sql
CREATE TABLE account (
id TEXT PRIMARY KEY,
userId TEXT NOT NULL,
accountId TEXT NOT NULL,
providerId TEXT NOT NULL,
accessToken TEXT,
refreshToken TEXT,
expiresAt TIMESTAMP,
scope TEXT,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (userId) REFERENCES user(id) ON DELETE CASCADE,
UNIQUE(providerId, accountId)
);
```
### Verification Table
```sql
CREATE TABLE verification (
id TEXT PRIMARY KEY,
identifier TEXT NOT NULL,
value TEXT NOT NULL,
expiresAt TIMESTAMP NOT NULL,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
## Schema Generation
### Using CLI
```bash
# Generate schema files
npx @better-auth/cli generate
# Specify adapter
npx @better-auth/cli generate --adapter drizzle
npx @better-auth/cli generate --adapter prisma
# Specify output
npx @better-auth/cli generate --output ./db/schema.ts
```
### Auto-migrate (Kysely only)
```bash
npx @better-auth/cli migrate
```
For other ORMs, apply generated schema manually.
## Custom Fields
Add custom fields to user table:
```ts
export const auth = betterAuth({
user: {
additionalFields: {
role: {
type: "string",
required: false,
defaultValue: "user"
},
phoneNumber: {
type: "string",
required: false
},
subscriptionTier: {
type: "string",
required: false
}
}
}
});
```
After adding fields:
```bash
npx @better-auth/cli generate
```
Update user with custom fields:
```ts
await authClient.updateUser({
role: "admin",
phoneNumber: "+1234567890"
});
```
## Plugin Schema Extensions
Plugins add their own tables/fields. Regenerate schema after adding plugins:
```bash
npx @better-auth/cli generate
```
### Two-Factor Plugin Tables
- `twoFactor`: Stores TOTP secrets, backup codes
### Passkey Plugin Tables
- `passkey`: Stores WebAuthn credentials
### Organization Plugin Tables
- `organization`: Organization data
- `member`: Organization members
- `invitation`: Pending invitations
## Migration Strategies
### Development
```bash
# Generate schema
npx @better-auth/cli generate
# Apply migrations (Kysely)
npx @better-auth/cli migrate
# Or manual (Prisma)
npx prisma migrate dev
# Or manual (Drizzle)
npx drizzle-kit push
```
### Production
```bash
# Review generated migration
npx @better-auth/cli generate
# Test in staging
# Apply to production with your ORM's migration tool
# Prisma
npx prisma migrate deploy
# Drizzle
npx drizzle-kit push
# Kysely
npx @better-auth/cli migrate
```
## Connection Pooling
### PostgreSQL
```ts
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // Max connections
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
```
### MySQL
```ts
import { createPool } from "mysql2/promise";
const pool = createPool({
connectionString: process.env.DATABASE_URL,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
```
## Database URLs
### PostgreSQL
```env
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
# Or with connection params
DATABASE_URL=postgresql://user:password@localhost:5432/dbname?schema=public&connection_limit=10
```
### MySQL
```env
DATABASE_URL=mysql://user:password@localhost:3306/dbname
```
### SQLite
```env
DATABASE_URL=file:./dev.db
# Or in-memory
DATABASE_URL=:memory:
```
### MongoDB
```env
MONGODB_URI=mongodb://localhost:27017/dbname
# Or Atlas
MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/dbname
```
## Performance Optimization
### Indexes
Better Auth CLI auto-generates essential indexes:
- `user.email` (unique)
- `session.userId`
- `account.userId`
- `account.providerId, accountId` (unique)
Add custom indexes for performance:
```sql
CREATE INDEX idx_session_expires ON session(expiresAt);
CREATE INDEX idx_user_created ON user(createdAt);
```
### Query Optimization
```ts
// Use connection pooling
// Enable query caching where applicable
// Monitor slow queries
export const auth = betterAuth({
advanced: {
defaultCookieAttributes: {
sameSite: "lax",
secure: true,
httpOnly: true
}
}
});
```
## Backup Strategies
### PostgreSQL
```bash
# Backup
pg_dump dbname > backup.sql
# Restore
psql dbname < backup.sql
```
### MySQL
```bash
# Backup
mysqldump -u root -p dbname > backup.sql
# Restore
mysql -u root -p dbname < backup.sql
```
### SQLite
```bash
# Copy file
cp dev.db dev.db.backup
# Or use backup command
sqlite3 dev.db ".backup backup.db"
```
### MongoDB
```bash
# Backup
mongodump --db=dbname --out=./backup
# Restore
mongorestore --db=dbname ./backup/dbname
```
## Best Practices
1. **Environment Variables**: Store credentials in env vars, never commit
2. **Connection Pooling**: Use pools for PostgreSQL/MySQL in production
3. **Migrations**: Use ORM migration tools, not raw SQL in production
4. **Indexes**: Add indexes for frequently queried fields
5. **Backups**: Automate daily backups in production
6. **SSL**: Use SSL/TLS for database connections in production
7. **Schema Sync**: Keep schema in sync across environments
8. **Testing**: Use separate database for tests (in-memory SQLite ideal)
9. **Monitoring**: Monitor query performance and connection pool usage
10. **Cleanup**: Periodically clean expired sessions/verifications
## Troubleshooting
### Connection Errors
```ts
// Add connection timeout
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
connectionTimeoutMillis: 5000
});
```
### Schema Mismatch
```bash
# Regenerate schema
npx @better-auth/cli generate
# Apply migrations
# For Prisma: npx prisma migrate dev
# For Drizzle: npx drizzle-kit push
```
### Migration Failures
- Check database credentials
- Verify database server is running
- Check for schema conflicts
- Review migration SQL manually
### Performance Issues
- Add indexes on foreign keys
- Enable connection pooling
- Monitor slow queries
- Consider read replicas for heavy read workloads

View File

@@ -0,0 +1,416 @@
# Email/Password Authentication
Email/password is built-in auth method in Better Auth. No plugins required for basic functionality.
## Server Configuration
### Basic Setup
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
autoSignIn: true, // Auto sign-in after signup (default: true)
requireEmailVerification: false, // Require email verification before login
sendResetPasswordToken: async ({ user, url }) => {
// Send password reset email
await sendEmail(user.email, url);
}
}
});
```
### Custom Password Requirements
```ts
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
password: {
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true
}
}
});
```
## Client Usage
### Sign Up
```ts
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.signUp.email({
email: "user@example.com",
password: "securePassword123",
name: "John Doe",
image: "https://example.com/avatar.jpg", // optional
callbackURL: "/dashboard" // optional
}, {
onSuccess: (ctx) => {
// ctx.data contains user and session
console.log("User created:", ctx.data.user);
},
onError: (ctx) => {
alert(ctx.error.message);
}
});
```
### Sign In
```ts
const { data, error } = await authClient.signIn.email({
email: "user@example.com",
password: "securePassword123",
callbackURL: "/dashboard",
rememberMe: true // default: true
}, {
onSuccess: () => {
// redirect or update UI
},
onError: (ctx) => {
console.error(ctx.error.message);
}
});
```
### Sign Out
```ts
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
router.push("/login");
}
}
});
```
## Email Verification
### Server Setup
```ts
export const auth = betterAuth({
emailVerification: {
sendVerificationEmail: async ({ user, url, token }) => {
// Send verification email
await sendEmail({
to: user.email,
subject: "Verify your email",
html: `Click <a href="${url}">here</a> to verify your email.`
});
},
sendOnSignUp: true, // Send verification email on signup
autoSignInAfterVerification: true // Auto sign-in after verification
},
emailAndPassword: {
enabled: true,
requireEmailVerification: true // Require verification before login
}
});
```
### Client Usage
```ts
// Send verification email
await authClient.sendVerificationEmail({
email: "user@example.com",
callbackURL: "/verify-success"
});
// Verify email with token
await authClient.verifyEmail({
token: "verification-token-from-email"
});
```
## Password Reset Flow
### Server Setup
```ts
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
sendResetPasswordToken: async ({ user, url, token }) => {
await sendEmail({
to: user.email,
subject: "Reset your password",
html: `Click <a href="${url}">here</a> to reset your password.`
});
}
}
});
```
### Client Flow
```ts
// Step 1: Request password reset
await authClient.forgetPassword({
email: "user@example.com",
redirectTo: "/reset-password"
});
// Step 2: Reset password with token
await authClient.resetPassword({
token: "reset-token-from-email",
password: "newSecurePassword123"
});
```
### Change Password (Authenticated)
```ts
await authClient.changePassword({
currentPassword: "oldPassword123",
newPassword: "newPassword456",
revokeOtherSessions: true // Optional: logout other sessions
});
```
## Username Authentication
Requires `username` plugin for username-based auth.
### Server Setup
```ts
import { betterAuth } from "better-auth";
import { username } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
username({
// Allow sign in with username or email
allowUsernameOrEmail: true
})
]
});
```
### Client Setup
```ts
import { createAuthClient } from "better-auth/client";
import { usernameClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [usernameClient()]
});
```
### Client Usage
```ts
// Sign up with username
await authClient.signUp.username({
username: "johndoe",
password: "securePassword123",
email: "john@example.com", // optional
name: "John Doe"
});
// Sign in with username
await authClient.signIn.username({
username: "johndoe",
password: "securePassword123"
});
// Sign in with username or email (if allowUsernameOrEmail: true)
await authClient.signIn.username({
username: "johndoe", // or "john@example.com"
password: "securePassword123"
});
```
## Framework Setup
### Next.js (App Router)
```ts
// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
```
### Next.js (Pages Router)
```ts
// pages/api/auth/[...all].ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export default toNextJsHandler(auth);
```
### Nuxt
```ts
// server/api/auth/[...all].ts
import { auth } from "~/utils/auth";
import { toWebRequest } from "better-auth/utils/web";
export default defineEventHandler((event) => {
return auth.handler(toWebRequest(event));
});
```
### SvelteKit
```ts
// hooks.server.ts
import { auth } from "$lib/auth";
import { svelteKitHandler } from "better-auth/svelte-kit";
export async function handle({ event, resolve }) {
return svelteKitHandler({ event, resolve, auth });
}
```
### Astro
```ts
// pages/api/auth/[...all].ts
import { auth } from "@/lib/auth";
export async function ALL({ request }: { request: Request }) {
return auth.handler(request);
}
```
### Hono
```ts
import { Hono } from "hono";
import { auth } from "./auth";
const app = new Hono();
app.on(["POST", "GET"], "/api/auth/*", (c) => {
return auth.handler(c.req.raw);
});
```
### Express
```ts
import express from "express";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./auth";
const app = express();
app.all("/api/auth/*", toNodeHandler(auth));
```
## Protected Routes
### Next.js Middleware
```ts
// middleware.ts
import { auth } from "@/lib/auth";
import { NextRequest, NextResponse } from "next/server";
export async function middleware(request: NextRequest) {
const session = await auth.api.getSession({
headers: request.headers
});
if (!session) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/profile/:path*"]
};
```
### SvelteKit Hooks
```ts
// hooks.server.ts
import { auth } from "$lib/auth";
import { redirect } from "@sveltejs/kit";
export async function handle({ event, resolve }) {
const session = await auth.api.getSession({
headers: event.request.headers
});
if (event.url.pathname.startsWith("/dashboard") && !session) {
throw redirect(303, "/login");
}
return resolve(event);
}
```
### Nuxt Middleware
```ts
// middleware/auth.ts
export default defineNuxtRouteMiddleware(async (to) => {
const { data: session } = await useAuthSession();
if (!session.value && to.path.startsWith("/dashboard")) {
return navigateTo("/login");
}
});
```
## User Profile Management
### Get Current User
```ts
const { data: session } = await authClient.getSession();
console.log(session.user);
```
### Update User Profile
```ts
await authClient.updateUser({
name: "New Name",
image: "https://example.com/new-avatar.jpg",
// Custom fields if defined in schema
});
```
### Delete User Account
```ts
await authClient.deleteUser({
password: "currentPassword", // Required for security
callbackURL: "/" // Redirect after deletion
});
```
## Best Practices
1. **Password Security**: Enforce strong password requirements
2. **Email Verification**: Enable for production to prevent spam
3. **Rate Limiting**: Prevent brute force attacks (see advanced-features.md)
4. **HTTPS**: Always use HTTPS in production
5. **Error Messages**: Don't reveal if email exists during login
6. **Session Security**: Use secure, httpOnly cookies
7. **CSRF Protection**: Better Auth handles this automatically
8. **Password Reset**: Set short expiration for reset tokens
9. **Account Lockout**: Consider implementing after N failed attempts
10. **Audit Logs**: Track auth events for security monitoring

View File

@@ -0,0 +1,430 @@
# OAuth Providers
Better Auth provides built-in OAuth 2.0 support for social authentication. No plugins required.
## Supported Providers
GitHub, Google, Apple, Discord, Facebook, Microsoft, Twitter/X, Spotify, Twitch, LinkedIn, Dropbox, GitLab, and more.
## Basic OAuth Setup
### Server Configuration
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
// Optional: custom scopes
scope: ["user:email", "read:user"]
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
scope: ["openid", "email", "profile"]
},
discord: {
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
}
}
});
```
### Client Usage
```ts
import { authClient } from "@/lib/auth-client";
// Basic sign in
await authClient.signIn.social({
provider: "github",
callbackURL: "/dashboard"
});
// With callbacks
await authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard",
errorCallbackURL: "/error",
newUserCallbackURL: "/welcome", // For first-time users
});
```
## Provider Configuration
### GitHub OAuth
1. Create OAuth App at https://github.com/settings/developers
2. Set Authorization callback URL: `http://localhost:3000/api/auth/callback/github`
3. Add credentials to `.env`:
```env
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret
```
### Google OAuth
1. Create project at https://console.cloud.google.com
2. Enable Google+ API
3. Create OAuth 2.0 credentials
4. Add authorized redirect URI: `http://localhost:3000/api/auth/callback/google`
5. Add credentials to `.env`:
```env
GOOGLE_CLIENT_ID=your_client_id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your_client_secret
```
### Discord OAuth
1. Create application at https://discord.com/developers/applications
2. Add OAuth2 redirect: `http://localhost:3000/api/auth/callback/discord`
3. Add credentials:
```env
DISCORD_CLIENT_ID=your_client_id
DISCORD_CLIENT_SECRET=your_client_secret
```
### Apple Sign In
```ts
export const auth = betterAuth({
socialProviders: {
apple: {
clientId: process.env.APPLE_CLIENT_ID!,
clientSecret: process.env.APPLE_CLIENT_SECRET!,
teamId: process.env.APPLE_TEAM_ID!,
keyId: process.env.APPLE_KEY_ID!,
privateKey: process.env.APPLE_PRIVATE_KEY!
}
}
});
```
### Microsoft/Azure AD
```ts
export const auth = betterAuth({
socialProviders: {
microsoft: {
clientId: process.env.MICROSOFT_CLIENT_ID!,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
tenantId: process.env.MICROSOFT_TENANT_ID, // Optional: for specific tenant
}
}
});
```
### Twitter/X OAuth
```ts
export const auth = betterAuth({
socialProviders: {
twitter: {
clientId: process.env.TWITTER_CLIENT_ID!,
clientSecret: process.env.TWITTER_CLIENT_SECRET!,
}
}
});
```
## Custom OAuth Provider
Add custom OAuth 2.0 provider:
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
socialProviders: {
customProvider: {
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/oauth/userinfo",
scope: ["email", "profile"],
// Map provider user data to Better Auth user
mapProfile: (profile) => ({
id: profile.id,
email: profile.email,
name: profile.name,
image: profile.avatar_url
})
}
}
});
```
## Account Linking
Link multiple OAuth providers to same user account.
### Server Setup
```ts
export const auth = betterAuth({
account: {
accountLinking: {
enabled: true,
trustedProviders: ["google", "github"] // Auto-link these providers
}
}
});
```
### Client Usage
```ts
// Link new provider to existing account
await authClient.linkSocial({
provider: "google",
callbackURL: "/profile"
});
// List linked accounts
const { data: session } = await authClient.getSession();
const accounts = session.user.accounts;
// Unlink account
await authClient.unlinkAccount({
accountId: "account-id"
});
```
## Token Management
### Access OAuth Tokens
```ts
// Server-side
const session = await auth.api.getSession({
headers: request.headers
});
const accounts = await auth.api.listAccounts({
userId: session.user.id
});
// Get specific provider token
const githubAccount = accounts.find(a => a.providerId === "github");
const accessToken = githubAccount.accessToken;
const refreshToken = githubAccount.refreshToken;
```
### Refresh Tokens
```ts
// Manually refresh OAuth token
const newToken = await auth.api.refreshToken({
accountId: "account-id"
});
```
### Use Provider API
```ts
// Example: Use GitHub token to fetch repos
const githubAccount = accounts.find(a => a.providerId === "github");
const response = await fetch("https://api.github.com/user/repos", {
headers: {
Authorization: `Bearer ${githubAccount.accessToken}`
}
});
const repos = await response.json();
```
## Advanced OAuth Configuration
### Custom Scopes
```ts
export const auth = betterAuth({
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
scope: [
"user:email",
"read:user",
"repo", // Access repositories
"gist" // Access gists
]
}
}
});
```
### State Parameter
Better Auth automatically handles OAuth state parameter for CSRF protection.
```ts
// Custom state validation
export const auth = betterAuth({
advanced: {
generateState: async () => {
// Custom state generation
return crypto.randomUUID();
},
validateState: async (state: string) => {
// Custom state validation
return true;
}
}
});
```
### PKCE Support
Better Auth automatically uses PKCE (Proof Key for Code Exchange) for supported providers.
```ts
export const auth = betterAuth({
socialProviders: {
customProvider: {
pkce: true, // Enable PKCE
// ... other config
}
}
});
```
## Error Handling
### Client-Side
```ts
await authClient.signIn.social({
provider: "github",
errorCallbackURL: "/auth/error"
}, {
onError: (ctx) => {
console.error("OAuth error:", ctx.error);
// Handle specific errors
if (ctx.error.code === "OAUTH_ACCOUNT_ALREADY_LINKED") {
alert("This account is already linked to another user");
}
}
});
```
### Server-Side
```ts
export const auth = betterAuth({
callbacks: {
async onOAuthError({ error, provider }) {
console.error(`OAuth error with ${provider}:`, error);
// Log to monitoring service
await logError(error);
}
}
});
```
## Callback URLs
### Development
```
http://localhost:3000/api/auth/callback/{provider}
```
### Production
```
https://yourdomain.com/api/auth/callback/{provider}
```
**Important:** Add all callback URLs to OAuth provider settings.
## UI Components
### Sign In Button (React)
```tsx
import { authClient } from "@/lib/auth-client";
export function SocialSignIn() {
const handleOAuth = async (provider: string) => {
await authClient.signIn.social({
provider,
callbackURL: "/dashboard"
});
};
return (
<div className="space-y-2">
<button onClick={() => handleOAuth("github")}>
Sign in with GitHub
</button>
<button onClick={() => handleOAuth("google")}>
Sign in with Google
</button>
<button onClick={() => handleOAuth("discord")}>
Sign in with Discord
</button>
</div>
);
}
```
## Best Practices
1. **Callback URLs**: Add all environments (dev, staging, prod) to OAuth app
2. **Scopes**: Request minimum scopes needed
3. **Token Storage**: Better Auth stores tokens securely in database
4. **Token Refresh**: Implement automatic token refresh for long-lived sessions
5. **Account Linking**: Enable for better UX when user signs in with different providers
6. **Error Handling**: Provide clear error messages for OAuth failures
7. **Provider Icons**: Use official brand assets for OAuth buttons
8. **Mobile Deep Links**: Configure deep links for mobile OAuth flows
9. **Email Matching**: Consider auto-linking accounts with same email
10. **Privacy**: Inform users what data you access from OAuth providers
## Common Issues
### Redirect URI Mismatch
Ensure callback URL in OAuth app matches exactly:
```
http://localhost:3000/api/auth/callback/github
```
### Missing Scopes
Add required scopes for email access:
```ts
scope: ["user:email"] // GitHub
scope: ["email"] // Google
```
### HTTPS Required
Some providers (Apple, Microsoft) require HTTPS callbacks. Use ngrok for local development:
```bash
ngrok http 3000
```
### CORS Errors
Configure CORS if frontend/backend on different domains:
```ts
export const auth = betterAuth({
advanced: {
corsOptions: {
origin: ["https://yourdomain.com"],
credentials: true
}
}
});
```

View File

@@ -0,0 +1,521 @@
#!/usr/bin/env python3
"""
Better Auth Initialization Script
Interactive script to initialize Better Auth configuration.
Supports multiple databases, ORMs, and authentication methods.
.env loading order: process.env > skill/.env > skills/.env > .claude/.env
"""
import os
import sys
import json
import secrets
from pathlib import Path
from typing import Optional, Dict, Any, List
from dataclasses import dataclass
@dataclass
class EnvConfig:
"""Environment configuration holder."""
secret: str
url: str
database_url: Optional[str] = None
github_client_id: Optional[str] = None
github_client_secret: Optional[str] = None
google_client_id: Optional[str] = None
google_client_secret: Optional[str] = None
class BetterAuthInit:
"""Better Auth configuration initializer."""
def __init__(self, project_root: Optional[Path] = None):
"""
Initialize the Better Auth configuration tool.
Args:
project_root: Project root directory. Auto-detected if not provided.
"""
self.project_root = project_root or self._find_project_root()
self.env_config: Optional[EnvConfig] = None
@staticmethod
def _find_project_root() -> Path:
"""
Find project root by looking for package.json.
Returns:
Path to project root.
Raises:
RuntimeError: If project root cannot be found.
"""
current = Path.cwd()
while current != current.parent:
if (current / "package.json").exists():
return current
current = current.parent
raise RuntimeError("Could not find project root (no package.json found)")
def _load_env_files(self) -> Dict[str, str]:
"""
Load environment variables from .env files in order.
Loading order: process.env > skill/.env > skills/.env > .claude/.env
Returns:
Dictionary of environment variables.
"""
env_vars = {}
# Define search paths in reverse priority order
skill_dir = Path(__file__).parent.parent
env_paths = [
self.project_root / ".claude" / ".env",
self.project_root / ".claude" / "skills" / ".env",
skill_dir / ".env",
]
# Load from files (lowest priority first)
for env_path in env_paths:
if env_path.exists():
env_vars.update(self._parse_env_file(env_path))
# Override with process environment (highest priority)
env_vars.update(os.environ)
return env_vars
@staticmethod
def _parse_env_file(path: Path) -> Dict[str, str]:
"""
Parse .env file into dictionary.
Args:
path: Path to .env file.
Returns:
Dictionary of key-value pairs.
"""
env_vars = {}
try:
with open(path, "r") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, value = line.split("=", 1)
# Remove quotes if present
value = value.strip().strip('"').strip("'")
env_vars[key.strip()] = value
except Exception as e:
print(f"Warning: Could not parse {path}: {e}")
return env_vars
@staticmethod
def generate_secret(length: int = 32) -> str:
"""
Generate cryptographically secure random secret.
Args:
length: Length of secret in bytes.
Returns:
Hex-encoded secret string.
"""
return secrets.token_hex(length)
def prompt_database(self) -> Dict[str, Any]:
"""
Prompt user for database configuration.
Returns:
Database configuration dictionary.
"""
print("\nDatabase Configuration")
print("=" * 50)
print("1. Direct Connection (PostgreSQL/MySQL/SQLite)")
print("2. Drizzle ORM")
print("3. Prisma")
print("4. Kysely")
print("5. MongoDB")
choice = input("\nSelect database option (1-5): ").strip()
db_configs = {
"1": self._prompt_direct_db,
"2": self._prompt_drizzle,
"3": self._prompt_prisma,
"4": self._prompt_kysely,
"5": self._prompt_mongodb,
}
handler = db_configs.get(choice)
if not handler:
print("Invalid choice. Defaulting to direct PostgreSQL.")
return self._prompt_direct_db()
return handler()
def _prompt_direct_db(self) -> Dict[str, Any]:
"""Prompt for direct database connection."""
print("\nDatabase Type:")
print("1. PostgreSQL")
print("2. MySQL")
print("3. SQLite")
db_type = input("Select (1-3): ").strip()
if db_type == "3":
db_path = input("SQLite file path [./dev.db]: ").strip() or "./dev.db"
return {
"type": "sqlite",
"import": "import Database from 'better-sqlite3';",
"config": f'database: new Database("{db_path}")'
}
elif db_type == "2":
db_url = input("MySQL connection string: ").strip()
return {
"type": "mysql",
"import": "import { createPool } from 'mysql2/promise';",
"config": f"database: createPool({{ connectionString: process.env.DATABASE_URL }})",
"env_var": ("DATABASE_URL", db_url)
}
else:
db_url = input("PostgreSQL connection string: ").strip()
return {
"type": "postgresql",
"import": "import { Pool } from 'pg';",
"config": "database: new Pool({ connectionString: process.env.DATABASE_URL })",
"env_var": ("DATABASE_URL", db_url)
}
def _prompt_drizzle(self) -> Dict[str, Any]:
"""Prompt for Drizzle ORM configuration."""
print("\nDrizzle Provider:")
print("1. PostgreSQL")
print("2. MySQL")
print("3. SQLite")
provider = input("Select (1-3): ").strip()
provider_map = {"1": "pg", "2": "mysql", "3": "sqlite"}
provider_name = provider_map.get(provider, "pg")
return {
"type": "drizzle",
"provider": provider_name,
"import": "import { drizzleAdapter } from 'better-auth/adapters/drizzle';\nimport { db } from '@/db';",
"config": f"database: drizzleAdapter(db, {{ provider: '{provider_name}' }})"
}
def _prompt_prisma(self) -> Dict[str, Any]:
"""Prompt for Prisma configuration."""
print("\nPrisma Provider:")
print("1. PostgreSQL")
print("2. MySQL")
print("3. SQLite")
provider = input("Select (1-3): ").strip()
provider_map = {"1": "postgresql", "2": "mysql", "3": "sqlite"}
provider_name = provider_map.get(provider, "postgresql")
return {
"type": "prisma",
"provider": provider_name,
"import": "import { prismaAdapter } from 'better-auth/adapters/prisma';\nimport { PrismaClient } from '@prisma/client';\n\nconst prisma = new PrismaClient();",
"config": f"database: prismaAdapter(prisma, {{ provider: '{provider_name}' }})"
}
def _prompt_kysely(self) -> Dict[str, Any]:
"""Prompt for Kysely configuration."""
return {
"type": "kysely",
"import": "import { kyselyAdapter } from 'better-auth/adapters/kysely';\nimport { db } from '@/db';",
"config": "database: kyselyAdapter(db, { provider: 'pg' })"
}
def _prompt_mongodb(self) -> Dict[str, Any]:
"""Prompt for MongoDB configuration."""
mongo_uri = input("MongoDB connection string: ").strip()
db_name = input("Database name: ").strip()
return {
"type": "mongodb",
"import": "import { mongodbAdapter } from 'better-auth/adapters/mongodb';\nimport { client } from '@/db';",
"config": f"database: mongodbAdapter(client, {{ databaseName: '{db_name}' }})",
"env_var": ("MONGODB_URI", mongo_uri)
}
def prompt_auth_methods(self) -> List[str]:
"""
Prompt user for authentication methods.
Returns:
List of selected auth method codes.
"""
print("\nAuthentication Methods")
print("=" * 50)
print("Select authentication methods (space-separated, e.g., '1 2 3'):")
print("1. Email/Password")
print("2. GitHub OAuth")
print("3. Google OAuth")
print("4. Discord OAuth")
print("5. Two-Factor Authentication (2FA)")
print("6. Passkeys (WebAuthn)")
print("7. Magic Link")
print("8. Username")
choices = input("\nYour selection: ").strip().split()
return [c for c in choices if c in "12345678"]
def generate_auth_config(
self,
db_config: Dict[str, Any],
auth_methods: List[str],
) -> str:
"""
Generate auth.ts configuration file content.
Args:
db_config: Database configuration.
auth_methods: Selected authentication methods.
Returns:
Generated TypeScript configuration code.
"""
imports = ["import { betterAuth } from 'better-auth';"]
plugins = []
plugin_imports = []
config_parts = []
# Database import
if db_config.get("import"):
imports.append(db_config["import"])
# Email/Password
if "1" in auth_methods:
config_parts.append(""" emailAndPassword: {
enabled: true,
autoSignIn: true
}""")
# OAuth providers
social_providers = []
if "2" in auth_methods:
social_providers.append(""" github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}""")
if "3" in auth_methods:
social_providers.append(""" google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}""")
if "4" in auth_methods:
social_providers.append(""" discord: {
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
}""")
if social_providers:
config_parts.append(f" socialProviders: {{\n{',\\n'.join(social_providers)}\n }}")
# Plugins
if "5" in auth_methods:
plugin_imports.append("import { twoFactor } from 'better-auth/plugins';")
plugins.append("twoFactor()")
if "6" in auth_methods:
plugin_imports.append("import { passkey } from 'better-auth/plugins';")
plugins.append("passkey()")
if "7" in auth_methods:
plugin_imports.append("import { magicLink } from 'better-auth/plugins';")
plugins.append("""magicLink({
sendMagicLink: async ({ email, url }) => {
// TODO: Implement email sending
console.log(`Magic link for ${email}: ${url}`);
}
})""")
if "8" in auth_methods:
plugin_imports.append("import { username } from 'better-auth/plugins';")
plugins.append("username()")
# Combine all imports
all_imports = imports + plugin_imports
# Build config
config_body = ",\n".join(config_parts)
if plugins:
plugins_str = ",\n ".join(plugins)
config_body += f",\n plugins: [\n {plugins_str}\n ]"
# Final output
return f"""{chr(10).join(all_imports)}
export const auth = betterAuth({{
{db_config["config"]},
{config_body}
}});
"""
def generate_env_file(
self,
db_config: Dict[str, Any],
auth_methods: List[str]
) -> str:
"""
Generate .env file content.
Args:
db_config: Database configuration.
auth_methods: Selected authentication methods.
Returns:
Generated .env file content.
"""
env_vars = [
f"BETTER_AUTH_SECRET={self.generate_secret()}",
"BETTER_AUTH_URL=http://localhost:3000",
]
# Database URL
if db_config.get("env_var"):
key, value = db_config["env_var"]
env_vars.append(f"{key}={value}")
# OAuth credentials
if "2" in auth_methods:
env_vars.extend([
"GITHUB_CLIENT_ID=your_github_client_id",
"GITHUB_CLIENT_SECRET=your_github_client_secret",
])
if "3" in auth_methods:
env_vars.extend([
"GOOGLE_CLIENT_ID=your_google_client_id",
"GOOGLE_CLIENT_SECRET=your_google_client_secret",
])
if "4" in auth_methods:
env_vars.extend([
"DISCORD_CLIENT_ID=your_discord_client_id",
"DISCORD_CLIENT_SECRET=your_discord_client_secret",
])
return "\n".join(env_vars) + "\n"
def run(self) -> None:
"""Run interactive initialization."""
print("=" * 50)
print("Better Auth Configuration Generator")
print("=" * 50)
# Load existing env
env_vars = self._load_env_files()
# Prompt for configuration
db_config = self.prompt_database()
auth_methods = self.prompt_auth_methods()
# Generate files
auth_config = self.generate_auth_config(db_config, auth_methods)
env_content = self.generate_env_file(db_config, auth_methods)
# Display output
print("\n" + "=" * 50)
print("Generated Configuration")
print("=" * 50)
print("\n--- auth.ts ---")
print(auth_config)
print("\n--- .env ---")
print(env_content)
# Offer to save
save = input("\nSave configuration files? (y/N): ").strip().lower()
if save == "y":
self._save_files(auth_config, env_content)
else:
print("Configuration not saved.")
def _save_files(self, auth_config: str, env_content: str) -> None:
"""
Save generated configuration files.
Args:
auth_config: auth.ts content.
env_content: .env content.
"""
# Save auth.ts
auth_locations = [
self.project_root / "lib" / "auth.ts",
self.project_root / "src" / "lib" / "auth.ts",
self.project_root / "utils" / "auth.ts",
self.project_root / "auth.ts",
]
print("\nWhere to save auth.ts?")
for i, loc in enumerate(auth_locations, 1):
print(f"{i}. {loc}")
print("5. Custom path")
choice = input("Select (1-5): ").strip()
if choice == "5":
custom_path = input("Enter path: ").strip()
auth_path = Path(custom_path)
else:
idx = int(choice) - 1 if choice.isdigit() else 0
auth_path = auth_locations[idx]
auth_path.parent.mkdir(parents=True, exist_ok=True)
auth_path.write_text(auth_config)
print(f"Saved: {auth_path}")
# Save .env
env_path = self.project_root / ".env"
if env_path.exists():
backup = self.project_root / ".env.backup"
env_path.rename(backup)
print(f"Backed up existing .env to {backup}")
env_path.write_text(env_content)
print(f"Saved: {env_path}")
print("\nNext steps:")
print("1. Run: npx @better-auth/cli generate")
print("2. Apply database migrations")
print("3. Mount API handler in your framework")
print("4. Create client instance")
def main() -> int:
"""
Main entry point.
Returns:
Exit code (0 for success, 1 for error).
"""
try:
initializer = BetterAuthInit()
initializer.run()
return 0
except KeyboardInterrupt:
print("\n\nOperation cancelled.")
return 1
except Exception as e:
print(f"\nError: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,15 @@
# Better Auth Skill Dependencies
# Python 3.10+ required
# No Python package dependencies - uses only standard library
# Testing dependencies (dev)
pytest>=8.0.0
pytest-cov>=4.1.0
pytest-mock>=3.12.0
# Note: This script generates Better Auth configuration
# The actual Better Auth library is installed via npm/pnpm/yarn:
# npm install better-auth
# pnpm add better-auth
# yarn add better-auth

View File

@@ -0,0 +1,421 @@
"""
Tests for better_auth_init.py
Covers main functionality with mocked I/O and file operations.
Target: >80% coverage
"""
import sys
import pytest
from pathlib import Path
from unittest.mock import Mock, patch, mock_open, MagicMock
from io import StringIO
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from better_auth_init import BetterAuthInit, EnvConfig, main
@pytest.fixture
def mock_project_root(tmp_path):
"""Create mock project root with package.json."""
(tmp_path / "package.json").write_text("{}")
return tmp_path
@pytest.fixture
def auth_init(mock_project_root):
"""Create BetterAuthInit instance with mock project root."""
return BetterAuthInit(project_root=mock_project_root)
class TestBetterAuthInit:
"""Test BetterAuthInit class."""
def test_init_with_project_root(self, mock_project_root):
"""Test initialization with explicit project root."""
init = BetterAuthInit(project_root=mock_project_root)
assert init.project_root == mock_project_root
assert init.env_config is None
def test_find_project_root_success(self, mock_project_root, monkeypatch):
"""Test finding project root successfully."""
monkeypatch.chdir(mock_project_root)
init = BetterAuthInit()
assert init.project_root == mock_project_root
def test_find_project_root_failure(self, tmp_path, monkeypatch):
"""Test failure to find project root."""
# Create path without package.json
no_package_dir = tmp_path / "no-package"
no_package_dir.mkdir()
monkeypatch.chdir(no_package_dir)
# Mock parent to stop infinite loop
with patch.object(Path, "parent", new_callable=lambda: property(lambda self: self)):
with pytest.raises(RuntimeError, match="Could not find project root"):
BetterAuthInit()
def test_generate_secret(self):
"""Test secret generation."""
secret = BetterAuthInit.generate_secret()
assert len(secret) == 64 # 32 bytes = 64 hex chars
assert all(c in "0123456789abcdef" for c in secret)
# Test custom length
secret = BetterAuthInit.generate_secret(length=16)
assert len(secret) == 32 # 16 bytes = 32 hex chars
def test_parse_env_file(self, tmp_path):
"""Test parsing .env file."""
env_content = """
# Comment
KEY1=value1
KEY2="value2"
KEY3='value3'
INVALID LINE
KEY4=value=with=equals
"""
env_file = tmp_path / ".env"
env_file.write_text(env_content)
result = BetterAuthInit._parse_env_file(env_file)
assert result["KEY1"] == "value1"
assert result["KEY2"] == "value2"
assert result["KEY3"] == "value3"
assert result["KEY4"] == "value=with=equals"
assert "INVALID" not in result
def test_parse_env_file_missing(self, tmp_path):
"""Test parsing missing .env file."""
result = BetterAuthInit._parse_env_file(tmp_path / "nonexistent.env")
assert result == {}
def test_load_env_files(self, auth_init, mock_project_root):
"""Test loading environment variables from multiple files."""
# Create .env files
claude_env = mock_project_root / ".claude" / ".env"
claude_env.parent.mkdir(parents=True, exist_ok=True)
claude_env.write_text("BASE_VAR=base\nOVERRIDE=claude")
skills_env = mock_project_root / ".claude" / "skills" / ".env"
skills_env.parent.mkdir(parents=True, exist_ok=True)
skills_env.write_text("OVERRIDE=skills\nSKILLS_VAR=skills")
# Mock process env (highest priority)
with patch.dict("os.environ", {"OVERRIDE": "process", "PROCESS_VAR": "process"}):
result = auth_init._load_env_files()
assert result["BASE_VAR"] == "base"
assert result["SKILLS_VAR"] == "skills"
assert result["OVERRIDE"] == "process" # Process env wins
assert result["PROCESS_VAR"] == "process"
def test_prompt_direct_db_sqlite(self, auth_init):
"""Test prompting for SQLite database."""
with patch("builtins.input", side_effect=["3", "./test.db"]):
config = auth_init._prompt_direct_db()
assert config["type"] == "sqlite"
assert "better-sqlite3" in config["import"]
assert "./test.db" in config["config"]
def test_prompt_direct_db_postgresql(self, auth_init):
"""Test prompting for PostgreSQL database."""
with patch("builtins.input", side_effect=["1", "postgresql://localhost/test"]):
config = auth_init._prompt_direct_db()
assert config["type"] == "postgresql"
assert "pg" in config["import"]
assert config["env_var"] == ("DATABASE_URL", "postgresql://localhost/test")
def test_prompt_direct_db_mysql(self, auth_init):
"""Test prompting for MySQL database."""
with patch("builtins.input", side_effect=["2", "mysql://localhost/test"]):
config = auth_init._prompt_direct_db()
assert config["type"] == "mysql"
assert "mysql2" in config["import"]
assert config["env_var"][0] == "DATABASE_URL"
def test_prompt_drizzle(self, auth_init):
"""Test prompting for Drizzle ORM."""
with patch("builtins.input", return_value="1"):
config = auth_init._prompt_drizzle()
assert config["type"] == "drizzle"
assert config["provider"] == "pg"
assert "drizzleAdapter" in config["import"]
assert "drizzleAdapter" in config["config"]
def test_prompt_prisma(self, auth_init):
"""Test prompting for Prisma."""
with patch("builtins.input", return_value="2"):
config = auth_init._prompt_prisma()
assert config["type"] == "prisma"
assert config["provider"] == "mysql"
assert "prismaAdapter" in config["import"]
assert "PrismaClient" in config["import"]
def test_prompt_kysely(self, auth_init):
"""Test prompting for Kysely."""
config = auth_init._prompt_kysely()
assert config["type"] == "kysely"
assert "kyselyAdapter" in config["import"]
def test_prompt_mongodb(self, auth_init):
"""Test prompting for MongoDB."""
with patch("builtins.input", side_effect=["mongodb://localhost/test", "mydb"]):
config = auth_init._prompt_mongodb()
assert config["type"] == "mongodb"
assert "mongodbAdapter" in config["import"]
assert "mydb" in config["config"]
assert config["env_var"] == ("MONGODB_URI", "mongodb://localhost/test")
def test_prompt_database(self, auth_init):
"""Test database prompting with different choices."""
# Test valid choice
with patch("builtins.input", side_effect=["3", "1"]):
config = auth_init.prompt_database()
assert config["type"] == "prisma"
# Test invalid choice (defaults to direct DB)
with patch("builtins.input", side_effect=["99", "1", "postgresql://localhost/test"]):
with patch("builtins.print"):
config = auth_init.prompt_database()
assert config["type"] == "postgresql"
def test_prompt_auth_methods(self, auth_init):
"""Test prompting for authentication methods."""
with patch("builtins.input", return_value="1 2 3 5 8"):
with patch("builtins.print"):
methods = auth_init.prompt_auth_methods()
assert methods == ["1", "2", "3", "5", "8"]
def test_prompt_auth_methods_invalid(self, auth_init):
"""Test filtering invalid auth method choices."""
with patch("builtins.input", return_value="1 99 abc 3"):
with patch("builtins.print"):
methods = auth_init.prompt_auth_methods()
assert methods == ["1", "3"]
def test_generate_auth_config_basic(self, auth_init):
"""Test generating basic auth config."""
db_config = {
"import": "import Database from 'better-sqlite3';",
"config": "database: new Database('./dev.db')"
}
auth_methods = ["1"] # Email/password only
config = auth_init.generate_auth_config(db_config, auth_methods)
assert "import { betterAuth }" in config
assert "emailAndPassword" in config
assert "enabled: true" in config
assert "better-sqlite3" in config
def test_generate_auth_config_with_oauth(self, auth_init):
"""Test generating config with OAuth providers."""
db_config = {
"import": "import { Pool } from 'pg';",
"config": "database: new Pool()"
}
auth_methods = ["1", "2", "3", "4"] # Email + GitHub + Google + Discord
config = auth_init.generate_auth_config(db_config, auth_methods)
assert "socialProviders" in config
assert "github:" in config
assert "google:" in config
assert "discord:" in config
assert "GITHUB_CLIENT_ID" in config
assert "GOOGLE_CLIENT_ID" in config
assert "DISCORD_CLIENT_ID" in config
def test_generate_auth_config_with_plugins(self, auth_init):
"""Test generating config with plugins."""
db_config = {"import": "", "config": "database: db"}
auth_methods = ["5", "6", "7", "8"] # 2FA, Passkey, Magic Link, Username
config = auth_init.generate_auth_config(db_config, auth_methods)
assert "plugins:" in config
assert "twoFactor" in config
assert "passkey" in config
assert "magicLink" in config
assert "username" in config
assert "from 'better-auth/plugins'" in config
def test_generate_env_file_basic(self, auth_init):
"""Test generating basic .env file."""
db_config = {"type": "sqlite"}
auth_methods = ["1"]
env_content = auth_init.generate_env_file(db_config, auth_methods)
assert "BETTER_AUTH_SECRET=" in env_content
assert "BETTER_AUTH_URL=http://localhost:3000" in env_content
assert len(env_content.split("\n")) >= 2
def test_generate_env_file_with_database_url(self, auth_init):
"""Test generating .env with database URL."""
db_config = {
"env_var": ("DATABASE_URL", "postgresql://localhost/test")
}
auth_methods = []
env_content = auth_init.generate_env_file(db_config, auth_methods)
assert "DATABASE_URL=postgresql://localhost/test" in env_content
def test_generate_env_file_with_oauth(self, auth_init):
"""Test generating .env with OAuth credentials."""
db_config = {}
auth_methods = ["2", "3", "4"] # GitHub, Google, Discord
env_content = auth_init.generate_env_file(db_config, auth_methods)
assert "GITHUB_CLIENT_ID=" in env_content
assert "GITHUB_CLIENT_SECRET=" in env_content
assert "GOOGLE_CLIENT_ID=" in env_content
assert "GOOGLE_CLIENT_SECRET=" in env_content
assert "DISCORD_CLIENT_ID=" in env_content
assert "DISCORD_CLIENT_SECRET=" in env_content
def test_save_files(self, auth_init, mock_project_root):
"""Test saving configuration files."""
auth_config = "// auth config"
env_content = "SECRET=test"
with patch("builtins.input", side_effect=["1"]):
auth_init._save_files(auth_config, env_content)
# Check auth.ts was saved
auth_path = mock_project_root / "lib" / "auth.ts"
assert auth_path.exists()
assert auth_path.read_text() == auth_config
# Check .env was saved
env_path = mock_project_root / ".env"
assert env_path.exists()
assert env_path.read_text() == env_content
def test_save_files_custom_path(self, auth_init, mock_project_root):
"""Test saving with custom path."""
auth_config = "// config"
env_content = "SECRET=test"
custom_path = str(mock_project_root / "custom" / "auth.ts")
with patch("builtins.input", side_effect=["5", custom_path]):
auth_init._save_files(auth_config, env_content)
assert Path(custom_path).exists()
def test_save_files_backup_existing_env(self, auth_init, mock_project_root):
"""Test backing up existing .env file."""
# Create existing .env
env_path = mock_project_root / ".env"
env_path.write_text("OLD_SECRET=old")
auth_config = "// config"
env_content = "NEW_SECRET=new"
with patch("builtins.input", return_value="1"):
auth_init._save_files(auth_config, env_content)
# Check backup was created
backup_path = mock_project_root / ".env.backup"
assert backup_path.exists()
assert backup_path.read_text() == "OLD_SECRET=old"
# Check new .env
assert env_path.read_text() == "NEW_SECRET=new"
def test_run_full_flow(self, auth_init, mock_project_root):
"""Test complete run flow."""
inputs = [
"1", # Direct DB
"1", # PostgreSQL
"postgresql://localhost/test",
"1 2", # Email + GitHub
"n" # Don't save
]
with patch("builtins.input", side_effect=inputs):
with patch("builtins.print"):
auth_init.run()
# Should complete without errors
# Files not saved because user chose 'n'
assert not (mock_project_root / "auth.ts").exists()
def test_run_save_files(self, auth_init, mock_project_root):
"""Test run flow with file saving."""
inputs = [
"1", # Direct DB
"3", # SQLite
"", # Default path
"1", # Email only
"y", # Save
"1" # Save location
]
with patch("builtins.input", side_effect=inputs):
with patch("builtins.print"):
auth_init.run()
# Check files were created
assert (mock_project_root / "lib" / "auth.ts").exists()
assert (mock_project_root / ".env").exists()
class TestMainFunction:
"""Test main entry point."""
def test_main_success(self, tmp_path, monkeypatch):
"""Test successful main execution."""
(tmp_path / "package.json").write_text("{}")
monkeypatch.chdir(tmp_path)
inputs = ["1", "3", "", "1", "n"]
with patch("builtins.input", side_effect=inputs):
with patch("builtins.print"):
exit_code = main()
assert exit_code == 0
def test_main_keyboard_interrupt(self, tmp_path, monkeypatch):
"""Test main with keyboard interrupt."""
(tmp_path / "package.json").write_text("{}")
monkeypatch.chdir(tmp_path)
with patch("builtins.input", side_effect=KeyboardInterrupt()):
with patch("builtins.print"):
exit_code = main()
assert exit_code == 1
def test_main_error(self, tmp_path, monkeypatch):
"""Test main with error."""
# No package.json - should fail
no_package = tmp_path / "no-package"
no_package.mkdir()
monkeypatch.chdir(no_package)
with patch.object(Path, "parent", new_callable=lambda: property(lambda self: self)):
with patch("sys.stderr", new_callable=StringIO):
exit_code = main()
assert exit_code == 1
if __name__ == "__main__":
pytest.main([__file__, "-v", "--cov=better_auth_init", "--cov-report=term-missing"])