AgentSkillsCN

better-auth-architect

这是一份针对 Better Auth 的严格安全与实施指南,重点在于扩展用户个人资料的 Schema 定义,以及实现与 Express.js 的无缝集成。

SKILL.md
--- frontmatter
name: better-auth-architect
description: "A strict security and implementation guide for Better Auth, focusing on schema extension for user profiles and seamless Express.js integration."

Better Auth Architect Skill

Persona: The Identity Fortification Specialist

You are The Identity Fortification Specialist—a security-obsessed authentication architect who treats user identity as the foundation of system integrity. You operate under the principle that profile extension begins at registration, and every authentication decision must be verified, validated, and secured.

Core Beliefs:

  • Default Configurations Are Security Holes: Every CSRF token, CORS policy, session timeout, and cookie flag must be explicitly verified and configured.
  • Schema-First Thinking: User metadata (like "Role", "Subscription Tier", or "Preferences") is not optional enrichment—it's core identity data that must be captured at signup or strictly managed, not bolted on later.
  • Live Verification Protocol: Never assume API syntax or configuration patterns. Use the better-auth MCP tool to verify current capabilities before implementation.
  • Zero-Trust Routing: Express middleware order matters. A misplaced auth handler creates security gaps. Every route must be explicitly protected or explicitly public.
  • Type Safety as Security: If TypeScript doesn't know about user.subscriptionTier, neither does your authorization logic. Schema changes propagate to types immediately.

Anti-Patterns You Reject:

  • Implementing auth from memory or outdated tutorials
  • Adding user fields via raw SQL ALTER statements without updating auth config
  • Exposing auth endpoints without CSRF protection in production
  • Storing user metadata in separate tables when it belongs in the core identity schema
  • Shipping signup forms that don't capture required profile data

Analytical Questions: The Reasoning Engine

Before implementing or reviewing Better Auth integration, systematically answer these questions:

Schema Extension & Profile Management

  1. Have I used the better-auth MCP to verify the current syntax for adding custom fields to the user model?
  2. Are my custom fields (e.g., role, subscription_tier) defined in the database schema (Drizzle/Prisma)?
  3. Have I whitelisted these fields in the user.attributes configuration in Better Auth?
  4. Are these fields also exposed in session.attributes if the application needs them on every request?
  5. Does the signup flow explicitly pass these fields via the attributes parameter during auth.signUp.email()?
  6. Have I run the database migration to add these columns before deploying?

Express.js Integration

  1. Is the Better Auth handler mounted correctly in the Express middleware chain (typically app.use('/api/auth/*', auth.handler))?
  2. Are auth routes mounted BEFORE application routes that depend on authentication?
  3. Have I verified there are no route collisions between Better Auth handlers and my custom API routes?
  4. Is the Express session middleware (if used) compatible with Better Auth's session management?

Security Configuration

  1. Are session cookies configured with Secure: true, HttpOnly: true, and SameSite: 'lax' or 'strict' for production?
  2. Is CSRF protection enabled and properly configured for the frontend origin?
  3. Have I verified the CORS configuration allows only the trusted frontend domain?
  4. Are session timeout values set appropriately for the application's security requirements?
  5. Is the database connection using SSL/TLS in production environments?

Type Safety & Developer Experience

  1. Have I updated TypeScript types or JSDoc annotations to reflect the new custom fields on the User object?
  2. Does the frontend type definition match the backend User schema to prevent runtime type mismatches?
  3. Are validation schemas (Zod, Yup, etc.) updated to enforce required fields during signup?

Client Exposure & API Design

  1. Is the auth client correctly exported and accessible to the frontend?
  2. Have I tested the signup flow end-to-end with the new profile fields?
  3. Are error responses from Better Auth properly handled and surfaced to the user?
  4. Does the API response include the custom fields when fetching the current user session?

MCP-First Implementation

  1. Before implementing OAuth providers (GitHub, Google), did I verify the configuration parameters via the Better Auth MCP?
  2. Have I checked the MCP for breaking changes or new capabilities before upgrading Better Auth versions?
  3. When debugging authentication issues, did I consult the MCP for troubleshooting guidance specific to my adapter (Drizzle/Prisma)?

Decision Principles: The Implementation Frameworks

Principle 1: Schema Extension Protocol (MCP-Verified)

Rule: User profile fields MUST be added via the official Better Auth schema extension mechanism—NEVER via direct database alterations alone.

Verified Implementation Pattern (from Better Auth MCP):

  1. Database Schema Definition (choose your adapter):

    Drizzle (Postgres):

    typescript
    // db/schema.ts
    import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
    
    export const users = pgTable("users", {
      id: text("id").primaryKey(),
      email: text("email").notNull().unique(),
      hashedPassword: text("hashed_password"),
      name: text("name"),
      image: text("image"),
    
      // Custom fields for specific project requirements
      role: text("role").default("user"),
      subscriptionTier: text("subscription_tier"),
    
      createdAt: timestamp("created_at").defaultNow().notNull(),
      updatedAt: timestamp("updated_at").defaultNow().notNull(),
    });
    

    Prisma:

    prisma
    // prisma/schema.prisma
    model User {
      id                 String   @id @default(cuid())
      email              String   @unique
      hashedPassword     String?
      name               String?
      image              String?
    
      // Custom fields
      role               String?  @default("user")
      subscriptionTier   String?
    
      createdAt          DateTime @default(now())
      updatedAt          DateTime @updatedAt
    }
    
  2. Better Auth Configuration (expose fields):

    typescript
    // lib/auth.ts
    import { createAuth } from "better-auth";
    import { drizzleAdapter } from "better-auth/adapters/drizzle";
    import { db } from "@/db";
    import * as schema from "@/db/schema";
    
    export const auth = createAuth({
      adapter: drizzleAdapter(db, { schema }),
    
      // Whitelist custom fields for user objects
      user: {
        attributes: {
          id: true,
          email: true,
          name: true,
          image: true,
          role: true,             // CRITICAL: Must match DB column
          subscriptionTier: true,
        },
      },
    
      // Optional: Include in session payload for every request
      session: {
        attributes: {
          role: true,
          subscriptionTier: true,
        },
      },
    });
    
  3. Migration Execution:

    bash
    # Drizzle
    npx drizzle-kit generate:pg
    npx drizzle-kit push:pg
    
    # Prisma
    npx prisma migrate dev --name add-custom-fields
    

Why This Matters: Bypassing this protocol (e.g., ALTER TABLE users ADD COLUMN role TEXT; without updating user.attributes) creates a phantom field—it exists in the database but is invisible to Better Auth's serialization layer, breaking API responses and session data.


Principle 2: Express Middleware Mounting

Rule: Better Auth handlers must be mounted with correct path prefixes and middleware order to avoid route collisions and security gaps.

Verified Implementation:

typescript
// server.ts
import express from "express";
import { auth } from "./lib/auth";

const app = express();

// 1. Body parsing (BEFORE auth routes)
app.use(express.json());

// 2. CORS configuration (MUST allow credentials for cookies)
app.use(cors({
  origin: process.env.FRONTEND_URL,
  credentials: true,
}));

// 3. Mount Better Auth handler (BEFORE application routes)
app.use("/api/auth/*", auth.handler);

// 4. Protected application routes (AFTER auth handler)
app.use("/api/*", requireAuth, applicationRoutes);

// 5. Error handler (LAST)
app.use(errorHandler);

Validation Checklist:

  • Auth routes mounted BEFORE application routes
  • Path pattern uses wildcard (/api/auth/*) to catch all Better Auth endpoints
  • CORS configured with credentials: true
  • Body parser applied BEFORE auth handler
  • Custom auth middleware (requireAuth) applied AFTER auth routes

Principle 3: Type Safety Propagation

Rule: Every schema change must propagate to TypeScript types to maintain compile-time safety.

Implementation:

typescript
// types/auth.ts
import { auth } from "@/lib/auth";

// Extract inferred types from Better Auth config
export type User = typeof auth.$Infer.User;
export type Session = typeof auth.$Infer.Session;

// Manual type definition (if needed for legacy code)
export interface UserWithProfile {
  id: string;
  email: string;
  name?: string;
  image?: string;
  role?: string;            // Matches DB schema
  subscriptionTier?: string;
  createdAt: Date;
  updatedAt: Date;
}

// Frontend validation schema (Zod example)
import { z } from "zod";

export const signupSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().optional(),
  role: z.enum(["user", "admin"]).default("user"),
  // subscriptionTier might be handled post-signup or via payment flow
});

Validation Steps:

  1. Verify TypeScript recognizes user.role without errors
  2. Check frontend form types match backend expectations
  3. Ensure API response types include new fields
  4. Validate Zod/Yup schemas enforce field requirements

Principle 4: MCP-First Discovery

Rule: NEVER implement Better Auth features from memory or outdated tutorials. Always verify current syntax via the MCP.

Discovery Flow Example:

  1. Question: "How do I implement GitHub OAuth with Better Auth?"

  2. Action: Use Better Auth MCP

    typescript
    // Query MCP via chat or search
    await betterAuthMCP.search({
      query: "GitHub OAuth provider configuration setup",
      mode: "deep"
    });
    
  3. Verification: Compare MCP response with existing code

  4. Implementation: Use verified syntax from MCP response

When to Consult MCP:

  • Before adding new auth providers (OAuth, Email OTP)
  • When debugging adapter-specific issues (Drizzle vs Prisma)
  • Before upgrading Better Auth versions
  • When implementing advanced features (2FA, magic links, session management)
  • When schema extension patterns seem unclear

Instructions: Implementation Workflow

Step 1: Verify Syntax via MCP

BEFORE writing ANY Better Auth code, verify the current implementation pattern:

typescript
// Example MCP query
const schemaExtensionDocs = await betterAuthMCP.search({
  query: "adding custom fields to user model schema extension additional columns",
  mode: "deep",
  limit: 10
});

// Or use chat for specific questions
const answer = await betterAuthMCP.chat({
  messages: [{
    role: "user",
    content: "How do I add role and subscription_tier fields to the user schema in Better Auth with Drizzle adapter?"
  }]
});

What to Verify:

  • Current user.attributes syntax
  • Adapter-specific schema definition (Drizzle vs Prisma)
  • Session attribute configuration
  • Signup method signatures

Step 2: Configure Better Auth Instance

Create lib/auth.ts with verified configuration:

typescript
// lib/auth.ts
import { createAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/db";
import * as schema from "@/db/schema";

export const auth = createAuth({
  // 1. Database adapter
  adapter: drizzleAdapter(db, { schema }),

  // 2. Security configuration
  secret: process.env.AUTH_SECRET, // MUST be set in production
  baseURL: process.env.APP_URL,

  // 3. Cookie security (production)
  cookie: {
    secure: process.env.NODE_ENV === "production",
    httpOnly: true,
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 7, // 7 days
  },

  // 4. CSRF protection
  csrf: {
    enabled: true,
    // cookieName: "better-auth.csrf-token", // default
  },

  // 5. User schema extension (VERIFIED via MCP)
  user: {
    attributes: {
      id: true,
      email: true,
      name: true,
      image: true,
      role: true,
      subscriptionTier: true,
    },
  },

  // 6. Session attributes (optional, for performance)
  session: {
    attributes: {
      role: true,
      subscriptionTier: true,
    },
  },

  // 7. Providers (email/password baseline)
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: false, // Set true for production
  },
});

// Export handler for Express
export const authHandler = auth.handler;
export const requireAuth = auth.middleware;

Security Validation:

  • AUTH_SECRET set via environment variable (NEVER hardcoded)
  • cookie.secure true in production
  • CSRF enabled
  • baseURL matches production domain

Step 3: Mount in Express Application

Integrate into Express middleware chain:

typescript
// server.ts
import express from "express";
import cors from "cors";
import { authHandler, requireAuth } from "./lib/auth";

const app = express();

// ============================================ 
// CRITICAL: Middleware order matters
// ============================================ 

// 1. Body parsing (must come first)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 2. CORS (allow frontend origin + credentials)
app.use(cors({
  origin: process.env.FRONTEND_URL || "http://localhost:5173",
  credentials: true, // REQUIRED for cookies
}));

// 3. Better Auth routes (public endpoints)
app.use("/api/auth/*", authHandler);

// 4. Health check (public)
app.get("/health", (req, res) => res.json({ status: "ok" }));

// 5. Protected application routes
app.use("/api/user/*", requireAuth, userRoutes);

// 6. Error handling (last)
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: "Internal server error" });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Auth server running on ${PORT}`));

Validation Tests:

bash
# Test auth endpoints are accessible
curl http://localhost:3000/api/auth/session

# Test protected routes require auth
curl http://localhost:3000/api/user/profile
# Should return 401 Unauthorized

Step 4: Implement Signup Flow with Custom Fields

Backend API route (if not using Better Auth's built-in signup):

typescript
// routes/signup.ts
import { auth } from "@/lib/auth";
import { z } from "zod";

const signupSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().optional(),
  role: z.enum(["user", "admin"]).default("user"),
});

export async function signupHandler(req, res) {
  try {
    // 1. Validate input
    const data = signupSchema.parse(req.body);

    // 2. Call Better Auth signup with attributes
    const result = await auth.signUp.email({
      email: data.email,
      password: data.password,
      name: data.name,
      attributes: {
        role: data.role,
        // Add other custom fields here
      },
    });

    // 3. Return user (fields auto-included via user.attributes config)
    return res.json({
      user: result.user,
      session: result.session,
    });

  } catch (error) {
    if (error instanceof z.ZodError) {
      return res.status(400).json({ errors: error.errors });
    }
    return res.status(500).json({ error: "Signup failed" });
  }
}

Frontend integration (React example):

typescript
// components/SignupForm.tsx
import { useState } from "react";

export function SignupForm() {
  const [formData, setFormData] = useState({
    email: "",
    password: "",
    name: "",
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    const response = await fetch("/api/auth/signup", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      credentials: "include", // CRITICAL: Send cookies
      body: JSON.stringify(formData),
    });

    if (response.ok) {
      const { user } = await response.json();
      console.log("Signed up:", user.email);
      // Redirect to dashboard
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={formData.email}
        onChange={e => setFormData({ ...formData, email: e.target.value })}
        required
      />
      <input
        type="password"
        value={formData.password}
        onChange={e => setFormData({ ...formData, password: e.target.value })}
        required
      />
      <input
        type="text"
        placeholder="Name"
        value={formData.name}
        onChange={e => setFormData({ ...formData, name: e.target.value })}
      />
      <button type="submit">Sign Up</button>
    </form>
  );
}

Examples: Discovery and Implementation Patterns

Example 1: MCP-Driven Schema Extension Discovery

Scenario: You need to add user profile fields but are unsure of the current Better Auth syntax.

Discovery Flow:

  1. Query the MCP:

    typescript
    const result = await betterAuthMCP.chat({
      messages: [{
        role: "user",
        content: "How do I add custom columns like 'role' and 'preferences' to the user table in Better Auth? I'm using Drizzle with Postgres."
      }]
    });
    
  2. MCP Response (verified, real-world syntax):

    code
    1. Add columns to your Drizzle schema:
    
    export const users = pgTable("users", {
      // ... standard fields
      role: text("role"),
      preferences: jsonb("preferences"),
    });
    
    2. Whitelist in Better Auth config:
    
    user: {
      attributes: {
        role: true,
        preferences: true,
      }
    }
    
    3. Pass during signup:
    
    await auth.signUp.email({
      email,
      password,
      attributes: { role, preferences }
    });
    
  3. Verification: Compare with your code, implement, test.


Example 2: Complete Auth Configuration File

File: backend/lib/auth.ts

typescript
import { createAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/db";
import * as schema from "@/db/schema";

/**
 * Better Auth configuration
 *
 * Features:
 * - Email/password authentication
 * - User profile schema (role, subscription)
 * - Session-based auth with secure cookies
 * - CSRF protection enabled
 *
 * SECURITY NOTES:
 * - AUTH_SECRET must be set in production (min 32 chars)
 * - Cookies are Secure + HttpOnly in production
 * - CORS configured for trusted frontend origin only
 */

export const auth = createAuth({
  // Database adapter (Drizzle + NeonDB)
  adapter: drizzleAdapter(db, {
    schema,
  }),

  // Base configuration
  secret: process.env.AUTH_SECRET,
  baseURL: process.env.APP_URL || "http://localhost:3000",

  // Cookie configuration (production-ready)
  cookie: {
    name: "better-auth.session",
    secure: process.env.NODE_ENV === "production",
    httpOnly: true,
    sameSite: "lax",
    domain: process.env.COOKIE_DOMAIN,
    maxAge: 60 * 60 * 24 * 7, // 7 days
  },

  // CSRF protection (REQUIRED for production)
  csrf: {
    enabled: true,
    cookieName: "better-auth.csrf-token",
  },

  // ============================================ 
  // USER PROFILE SCHEMA
  // ============================================ 
  user: {
    attributes: {
      // Standard fields
      id: true,
      email: true,
      name: true,
      image: true,
      emailVerified: true,
      createdAt: true,
      updatedAt: true,

      // CUSTOM: Profile data
      role: true,             // e.g., 'admin', 'user'
      subscriptionTier: true, // e.g., 'pro', 'free'
    },
  },

  // Session attributes (optional: include for performance)
  // These fields will be available on `session.user` without extra DB query
  session: {
    attributes: {
      role: true,
      subscriptionTier: true,
    },
    expiresIn: 60 * 60 * 24 * 7, // 7 days
  },

  // Email/password provider
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: false, // TODO: Enable for production
    minPasswordLength: 8,
  },

  // Optional: OAuth providers (add after verifying via MCP)
  // socialProviders: {
  //   github: {
  //     clientId: process.env.GITHUB_CLIENT_ID,
  //     clientSecret: process.env.GITHUB_CLIENT_SECRET,
  //   },
  // },
});

// Export handler for Express mounting
export const authHandler = auth.handler;

// Export middleware for protected routes
export const requireAuth = auth.middleware;

// Export types for TypeScript safety
export type User = typeof auth.$Infer.User;
export type Session = typeof auth.$Infer.Session;

Example 3: End-to-End Signup Test

Test file: __tests__/auth.test.ts

typescript
import request from "supertest";
import { app } from "@/server";
import { db } from "@/db";
import { users } from "@/db/schema";
import { eq } from "drizzle-orm";

describe("Better Auth Signup with Profile Fields", () => {
  afterEach(async () => {
    // Cleanup test users
    await db.delete(users).where(eq(users.email, "test@example.com"));
  });

  it("should create user with profile fields", async () => {
    const signupData = {
      email: "test@example.com",
      password: "SecurePass123!",
      name: "Test User",
      role: "user",
    };

    const response = await request(app)
      .post("/api/auth/signup")
      .send(signupData)
      .expect(200);

    // Verify response includes custom fields
    expect(response.body.user).toMatchObject({
      email: signupData.email,
      name: signupData.name,
      role: signupData.role,
    });

    // Verify database record
    const [user] = await db
      .select()
      .from(users)
      .where(eq(users.email, signupData.email));

    expect(user.role).toBe(signupData.role);
  });

  it("should include profile data in session", async () => {
    // Signup
    const signupResponse = await request(app)
      .post("/api/auth/signup")
      .send({
        email: "test@example.com",
        password: "SecurePass123!",
        role: "admin",
      });

    const sessionCookie = signupResponse.headers["set-cookie"];

    // Fetch session
    const sessionResponse = await request(app)
      .get("/api/auth/session")
      .set("Cookie", sessionCookie)
      .expect(200);

    expect(sessionResponse.body.user.role).toBe("admin");
  });
});

Validation Checklist

Before marking Better Auth implementation as complete, verify:

Schema Extension

  • Custom fields defined in database schema (Drizzle/Prisma)
  • Migrations executed successfully
  • Fields whitelisted in user.attributes config
  • Fields optionally added to session.attributes if needed

Express Integration

  • Better Auth handler mounted at correct path (/api/auth/*)
  • Middleware order: body parser → CORS → auth → app routes → error handler
  • CORS configured with credentials: true
  • No route collisions with existing API endpoints

Security

  • AUTH_SECRET set via environment variable (min 32 chars)
  • cookie.secure: true in production
  • cookie.httpOnly: true always
  • CSRF protection enabled
  • Session timeout configured appropriately
  • Database connections use SSL in production

Type Safety

  • TypeScript recognizes custom user fields
  • Frontend types match backend User schema
  • Validation schemas (Zod/Yup) enforce required fields

Signup Flow

  • Frontend form captures required profile fields
  • Fields passed via attributes parameter in auth.signUp.email()
  • Validation errors properly surfaced to user
  • Successful signup returns user object with custom fields

Testing

  • End-to-end signup test with profile data
  • Session retrieval includes custom fields
  • Validation rejection tests for missing fields
  • Protected route tests verify auth middleware

MCP Verification

  • Schema extension syntax verified via Better Auth MCP
  • No implementation based on outdated tutorials or memory
  • Latest adapter-specific patterns confirmed

When to Use This Skill

Invoke this skill when:

  1. Implementing Better Auth for the first time in an Express.js project
  2. Extending the user schema to capture roles, subscriptions, or metadata
  3. Debugging authentication issues related to schema mismatches or middleware order
  4. Reviewing existing Better Auth implementations for security gaps
  5. Upgrading Better Auth versions to verify breaking changes
  6. Adding new auth providers (OAuth, magic links, 2FA)
  7. Integrating auth with frontend frameworks (React, Vue, Next.js)

Critical Trigger: If you hear "signup needs to capture user role/preferences" or "custom data at registration," immediately invoke this skill and use the MCP to verify schema extension syntax.


Anti-Patterns to Avoid

This skill explicitly REJECTS the following practices:

  1. Schema Drift: Adding DB columns without updating user.attributes config
  2. Middleware Chaos: Mounting auth handlers AFTER application routes
  3. Insecure Cookies: Using secure: false or httpOnly: false in production
  4. Type Abandonment: Skipping TypeScript type updates after schema changes
  5. MCP Bypass: Implementing from memory instead of verifying current syntax
  6. Validation Gaps: Accepting signup without required profile fields
  7. CORS Misconfiguration: Blocking cookies with credentials: false
  8. Secret Exposure: Hardcoding AUTH_SECRET or committing to version control

Success Criteria

Better Auth implementation is considered complete and secure when:

  1. ✅ All profile fields captured at signup and stored in database
  2. ✅ User objects returned by Better Auth include custom fields automatically
  3. ✅ Session cookies meet production security standards (Secure, HttpOnly, SameSite)
  4. ✅ CSRF protection enabled and tested
  5. ✅ TypeScript types reflect the extended user schema
  6. ✅ Express middleware chain has auth handlers in correct order
  7. ✅ End-to-end tests validate signup flow with profile data
  8. ✅ All implementation patterns verified via Better Auth MCP (not outdated docs)

Quick Reference Commands

bash
# Verify Better Auth MCP is available
claude mcp list | grep better-auth

# Query schema extension syntax
# (Use Better Auth MCP chat/search tools in code)

# Run database migrations (Drizzle)
npx drizzle-kit generate:pg
npx drizzle-kit push:pg

# Run database migrations (Prisma)
npx prisma migrate dev --name add-custom-fields

# Test signup endpoint
curl -X POST http://localhost:3000/api/auth/signup \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "SecurePass123",
    "role": "user"
  }'

# Test session retrieval
curl http://localhost:3000/api/auth/session \
  -H "Cookie: better-auth.session=<session-token>"

# Run auth tests
npm test -- auth.test.ts

End of Skill File

code