docs: add comprehensive learning guides for payment gateway abstraction

Created four detailed guides to help understand better-auth patterns and
apply them to building a payment gateway abstraction library:

- LEARNING_GUIDE.md: 17-file study roadmap organized in 5 phases
- OAUTH_TO_PAYMENT_MAPPING.md: Mental model showing OAuth-to-Payment parallels
- QUICK_START_IMPLEMENTATION.md: Complete implementation guide with code
- STUDY_CHECKLIST.md: Interactive checklist to track learning progress

These guides show how to learn from better-auth's provider abstraction,
plugin system, and configuration patterns to build a "better-payments"
library that makes integrating Stripe, Flutterwave, PayStack, and other
payment gateways as simple as better-auth makes OAuth providers.
This commit is contained in:
Claude
2025-11-15 16:22:57 +00:00
parent 7aa296e6cb
commit e49fc2b9fc
4 changed files with 1988 additions and 0 deletions
+490
View File
@@ -0,0 +1,490 @@
# Better-Auth Learning Guide for Payment Gateway Project
This guide will help you understand the better-auth codebase to build your payment gateway abstraction library.
## 📚 Learning Path - Study These Files in Order
### **PHASE 1: Understanding Provider Abstraction** (START HERE)
#### 1. Provider Interface Definition
**File:** `packages/core/src/oauth2/oauth-provider.ts`
**Lines to focus on:** 14-83
**What to learn:**
- How a provider interface is structured
- What methods every provider must implement
- How TypeScript generics are used for type safety
- The difference between required and optional methods
**Key concepts:**
```typescript
- OAuthProvider<T, O> interface
- createAuthorizationURL() - how providers start flows
- validateAuthorizationCode() - how providers verify results
- getUserInfo() - how providers fetch data
- Optional methods (refreshAccessToken, revokeToken)
```
**Your equivalent:**
```typescript
PaymentProvider<T, O>
- createPaymentIntent()
- verifyPayment()
- handleWebhook()
- Optional: refundPayment(), createSubscription()
```
---
#### 2. Simple Provider Implementation
**File:** `packages/core/src/social-providers/github.ts`
**Lines to focus on:** 1-200
**What to learn:**
- How to implement the provider interface
- How to define provider-specific options
- How to make API calls to external services
- How to transform external data to standard format
**Key concepts:**
```typescript
- GithubProfile interface (provider-specific data)
- GithubOptions interface (extends ProviderOptions)
- github() factory function
- API endpoint configuration
- Error handling
```
**Why GitHub first:** Simpler than Google (no OIDC), shows basic patterns clearly
---
#### 3. Complex Provider Implementation
**File:** `packages/core/src/social-providers/google.ts`
**Lines to focus on:** 1-250
**What to learn:**
- Advanced provider features (ID token verification)
- Provider-specific options (accessType, display, hd)
- How to handle different authentication flows
- Token refresh implementation
**Key concepts:**
```typescript
- GoogleProfile interface
- GoogleOptions with provider-specific fields
- verifyIdToken() for OIDC
- refreshAccessToken() implementation
- Using helper functions (createAuthorizationURL, validateAuthorizationCode)
```
---
#### 4. Provider with Environment Handling
**File:** `packages/core/src/social-providers/paypal.ts`
**Lines to focus on:** 1-150
**What to learn:**
- How to handle sandbox vs production environments
- Dynamic endpoint configuration
- Environment-based API URLs
**Key concepts:**
```typescript
- PayPalOptions with environment field
- Conditional endpoint URLs (sandbox vs live)
- Environment-aware token endpoints
```
**Why this matters for you:** Payment gateways also have sandbox/production modes!
---
#### 5. Provider Registry and Exports
**File:** `packages/core/src/social-providers/index.ts`
**Lines to focus on:** 1-100
**What to learn:**
- How to organize multiple providers
- Type-safe provider registration
- Creating enums from provider names
- Configuration type generation
**Key concepts:**
```typescript
- socialProviders object (registry)
- socialProviderList array
- SocialProviders type (for configuration)
- Type-safe provider keys
```
---
### **PHASE 2: Understanding Configuration System**
#### 6. Main Configuration Interface
**File:** `packages/core/src/types/init-options.ts`
**Lines to focus on:** 1-300
**What to learn:**
- How to structure main configuration
- Nested configuration objects
- Optional vs required fields
- Default values and documentation
**Key concepts:**
```typescript
- BetterAuthOptions interface
- socialProviders configuration
- emailAndPassword configuration
- plugins array
- advanced options
```
**Your equivalent:**
```typescript
PaymentGatewayOptions
- providers: PaymentProviders
- defaultCurrency
- webhooks
- plugins
- advanced
```
---
#### 7. Provider Options Type
**File:** `packages/core/src/oauth2/oauth-provider.ts`
**Lines to focus on:** 85-120
**What to learn:**
- Base options that all providers share
- How to allow provider-specific extensions
- Generic type parameters for flexibility
**Key concepts:**
```typescript
- ProviderOptions<Profile> interface
- clientId, clientSecret (common to all)
- getUserInfo override option
- mapProfileToUser customization
```
---
### **PHASE 3: Understanding the Core Factory**
#### 8. Main Entry Point
**File:** `packages/better-auth/src/auth/auth.ts`
**Lines to focus on:** 1-50
**What to learn:**
- How the public API is exposed
- Simple wrapper pattern
- Type inference from options
**Key concepts:**
```typescript
- betterAuth<Options>() function
- Generic type parameter
- Delegation to createBetterAuth
- Return type: Auth<Options>
```
**Your equivalent:**
```typescript
export const betterPayments = <Options extends PaymentGatewayOptions>(
options: Options
) => {
return createPaymentGateway(options);
};
```
---
#### 9. Core Factory Function
**File:** `packages/better-auth/src/auth/base.ts`
**Lines to focus on:** 1-150
**What to learn:**
- How to initialize the system
- Request handler creation
- Plugin aggregation
- Error code collection
**Key concepts:**
```typescript
- createBetterAuth() factory
- initFn for async initialization
- handler: async (request: Request)
- Plugin error code aggregation
- Context management
```
---
#### 10. Context Creation
**File:** `packages/better-auth/src/context/create-context.ts`
**Lines to focus on:** 1-300
**What to learn:**
- How to instantiate providers from config
- Provider validation
- Context object structure
**Key concepts:**
```typescript
- Provider instantiation loop
- Config validation (checking clientId, clientSecret)
- Filtering disabled providers
- Creating provider instances from registry
```
**This is where:**
```typescript
socialProviders: {
google: { clientId: "...", clientSecret: "..." }
}
// Becomes:
const provider = socialProviders["google"](config);
```
---
### **PHASE 4: Understanding Plugin System**
#### 11. Plugin Interface
**File:** `packages/core/src/types/plugin.ts`
**Lines to focus on:** 1-200
**What to learn:**
- Plugin lifecycle hooks
- How plugins extend functionality
- Database schema definition
- Custom endpoints
**Key concepts:**
```typescript
- BetterAuthPlugin interface
- init() lifecycle hook
- endpoints object
- schema definition
- hooks (before/after)
- $ERROR_CODES
```
---
#### 12. Simple Plugin Example
**File:** `packages/better-auth/src/plugins/bearer/index.ts`
**Lines to focus on:** 1-200
**What to learn:**
- How to create a simple plugin
- Request/response transformation
- Middleware pattern
**Key concepts:**
```typescript
- bearer() plugin factory
- hooks.before and hooks.after
- matcher() function
- handler() implementation
- Header manipulation
```
**Why bearer:** Simple, focused plugin that shows core patterns
---
#### 13. Complex Plugin Example
**File:** `packages/stripe/src/index.ts`
**Lines to focus on:** 1-500
**What to learn:**
- How to integrate external services
- Database schema in plugins
- Custom endpoints
- Error handling
**Key concepts:**
```typescript
- stripe() plugin factory with options
- schema.subscription definition
- Multiple endpoints (get-plans, subscribe, cancel)
- External API integration (Stripe SDK)
- Error codes definition
```
**This is EXACTLY what you're building!** A Stripe plugin but as the core concept.
---
#### 14. Plugin Initialization
**File:** `packages/better-auth/src/context/helpers.ts`
**Lines to focus on:** 1-150 (runPluginInit function)
**What to learn:**
- How plugins are initialized
- How plugins can modify context and options
- Plugin composition
**Key concepts:**
```typescript
- runPluginInit() function
- Iterating through plugins
- Calling plugin.init()
- Merging options from plugins
- Deep merging with defu
```
---
### **PHASE 5: Understanding Package Structure**
#### 15. Monorepo Workspace Config
**File:** `pnpm-workspace.yaml`
**What to learn:**
- How to structure a monorepo
- Package organization
---
#### 16. Core Package Structure
**File:** `packages/core/package.json`
**What to learn:**
- How to define exports
- Dependencies vs peerDependencies
- Package naming
---
#### 17. Main Package Structure
**File:** `packages/better-auth/package.json`
**What to learn:**
- How to export plugins
- Subpath exports pattern
- Build configuration
---
## 🎯 Quick Reference by Concept
### **Want to learn:** Provider Abstraction
**Read:** Files 1, 2, 3, 4, 5 (in order)
### **Want to learn:** Configuration System
**Read:** Files 6, 7, 10
### **Want to learn:** Main Entry Point
**Read:** Files 8, 9
### **Want to learn:** Plugin System
**Read:** Files 11, 12, 13, 14
### **Want to learn:** Project Structure
**Read:** Files 15, 16, 17
---
## 📝 Study Notes Template
For each file, take notes using this template:
```markdown
### File: [filename]
**Main Purpose:**
[What does this file do?]
**Key Interfaces/Types:**
- [Type name]: [What it represents]
- [Type name]: [What it represents]
**Key Functions:**
- [Function name]: [What it does]
- [Function name]: [What it does]
**How I'll use this pattern:**
[Your notes on how to apply this to payment gateways]
**Questions:**
[Anything you don't understand]
```
---
## 🚀 Practical Exercise
After studying files 1-5, try this:
**Exercise 1:** Design your `PaymentProvider` interface
- Model it after `OAuthProvider`
- Define required methods (create, verify, webhook)
- Define optional methods (refund, subscription)
**Exercise 2:** Implement a Stripe provider
- Model it after `google.ts`
- Use the Stripe SDK
- Follow the same pattern
**Exercise 3:** Implement a Flutterwave provider
- Model it after `github.ts`
- Use fetch for API calls
- Transform Flutterwave responses to your standard format
**Exercise 4:** Create a provider registry
- Model it after `social-providers/index.ts`
- Export all your providers
- Create type-safe configuration
---
## ⏱️ Estimated Time
- **Phase 1 (Provider Abstraction):** 3-4 hours
- **Phase 2 (Configuration):** 1-2 hours
- **Phase 3 (Core Factory):** 2-3 hours
- **Phase 4 (Plugin System):** 3-4 hours
- **Phase 5 (Package Structure):** 1 hour
**Total:** ~10-14 hours of focused study
---
## 💡 Pro Tips
1. **Don't read everything at once** - Follow the phases
2. **Try implementing as you learn** - Build your version alongside
3. **Focus on patterns, not details** - You don't need to understand every line
4. **Skip complex parts initially** - Come back to advanced features later
5. **Compare OAuth flow to Payment flow** - They're similar:
- OAuth: redirect → callback → get token → get user
- Payment: create intent → redirect → webhook → verify payment
---
## 🎓 What You'll Understand
After studying these files, you'll know:
✅ How to create a provider abstraction
✅ How to make providers pluggable
✅ How to create type-safe configuration
✅ How to build a factory function
✅ How to create a plugin system
✅ How to structure a monorepo
✅ How to make your library framework-agnostic
---
## 🤔 Common Questions
**Q: Do I need to understand everything in better-auth?**
A: No! Focus on the provider pattern and plugin system. Skip auth-specific logic.
**Q: Should I copy the code?**
A: Copy the **patterns**, not the code. Adapt it for payments.
**Q: What about the database stuff?**
A: Optional for MVP. Start with just payment provider abstraction.
**Q: How similar should my library be?**
A: Very similar in architecture, different in domain (payments vs auth).
---
## 📞 Next Steps
1. Read Phase 1 files (1-5)
2. Take notes using the template above
3. Try Exercise 1-2
4. Share your progress and ask questions!
Good luck! 🚀
+455
View File
@@ -0,0 +1,455 @@
# OAuth to Payment Gateway Mapping
This document shows how OAuth concepts in better-auth map to Payment Gateway concepts in your library.
## 🔄 Core Concept Mapping
| OAuth Concept (better-auth) | Payment Concept (your library) | Why It's Similar |
|----------------------------|--------------------------------|------------------|
| **OAuth Provider** (Google, GitHub) | **Payment Gateway** (Stripe, Flutterwave) | Both are external services with different APIs |
| **Authorization URL** | **Payment Intent / Checkout URL** | Both redirect user to external service |
| **OAuth Callback** | **Payment Webhook** | Both receive confirmation from external service |
| **Access Token** | **Payment ID / Transaction ID** | Both are identifiers for the session/transaction |
| **User Profile** | **Payment Details** | Both are data returned from the service |
| **Refresh Token** | **Subscription Renewal** | Both handle recurring/long-term access |
---
## 📋 Interface Comparison
### Better-Auth: OAuthProvider Interface
```typescript
export interface OAuthProvider {
id: string; // "google", "github"
name: string; // "Google", "GitHub"
// Start the OAuth flow
createAuthorizationURL: (data: {
state: string;
codeVerifier: string;
scopes?: string[];
redirectURI: string;
}) => Promise<URL>;
// Verify the callback
validateAuthorizationCode: (data: {
code: string;
redirectURI: string;
}) => Promise<OAuth2Tokens>;
// Get user data
getUserInfo: (token: OAuth2Tokens) => Promise<{
user: OAuth2UserInfo;
data: T;
}>;
// Optional: refresh access
refreshAccessToken?: (refreshToken: string) => Promise<OAuth2Tokens>;
}
```
### Your Library: PaymentProvider Interface
```typescript
export interface PaymentProvider {
id: string; // "stripe", "flutterwave"
name: string; // "Stripe", "Flutterwave"
// Start the payment flow
createPaymentIntent: (data: {
amount: number;
currency: string;
customerId?: string;
metadata?: Record<string, any>;
}) => Promise<PaymentIntent>;
// Verify the payment
verifyPayment: (paymentId: string) => Promise<PaymentStatus>;
// Handle provider callback
handleWebhook: (payload: unknown, signature: string) => Promise<WebhookEvent>;
// Optional: handle refunds
refundPayment?: (paymentId: string, amount?: number) => Promise<Refund>;
}
```
**See the parallel?** Both follow the same pattern:
1. **Initiate** (create URL / create intent)
2. **Verify** (validate code / verify payment)
3. **Get Data** (get user info / handle webhook)
4. **Optional Features** (refresh token / refund)
---
## 🔄 Flow Comparison
### OAuth Flow (better-auth)
```
1. User clicks "Login with Google"
↓
2. createAuthorizationURL() generates redirect URL
↓
3. User redirects to Google
↓
4. User approves on Google's site
↓
5. Google redirects back with code
↓
6. validateAuthorizationCode() exchanges code for tokens
↓
7. getUserInfo() fetches user profile
↓
8. User is logged in
```
### Payment Flow (your library)
```
1. User clicks "Pay with Stripe"
↓
2. createPaymentIntent() generates checkout session
↓
3. User redirects to Stripe checkout
↓
4. User pays on Stripe's site
↓
5. Stripe sends webhook to your server
↓
6. handleWebhook() verifies webhook signature
↓
7. verifyPayment() confirms payment status
↓
8. Payment is complete
```
**Same flow, different domain!**
---
## 📦 Provider Implementation Comparison
### Better-Auth: Google Provider
```typescript
export const google = (options: GoogleOptions) => {
return {
id: "google",
name: "Google",
async createAuthorizationURL({ state, scopes, redirectURI }) {
const url = new URL("https://accounts.google.com/o/oauth2/auth");
url.searchParams.set("client_id", options.clientId);
url.searchParams.set("redirect_uri", redirectURI);
url.searchParams.set("scope", scopes.join(" "));
url.searchParams.set("state", state);
return url;
},
async validateAuthorizationCode({ code, redirectURI }) {
const response = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
body: new URLSearchParams({
code,
client_id: options.clientId,
client_secret: options.clientSecret,
redirect_uri: redirectURI,
grant_type: "authorization_code",
}),
});
return await response.json();
},
async getUserInfo(token) {
const user = decodeJwt(token.idToken);
return {
user: {
id: user.sub,
email: user.email,
name: user.name,
},
data: user,
};
},
};
};
```
### Your Library: Stripe Provider
```typescript
export const stripe = (options: StripeOptions) => {
const stripeClient = new Stripe(options.apiKey);
return {
id: "stripe",
name: "Stripe",
async createPaymentIntent({ amount, currency, customerId, metadata }) {
const paymentIntent = await stripeClient.paymentIntents.create({
amount,
currency,
customer: customerId,
metadata,
});
return {
id: paymentIntent.id,
clientSecret: paymentIntent.client_secret,
amount: paymentIntent.amount,
status: paymentIntent.status,
};
},
async verifyPayment(paymentId) {
const paymentIntent = await stripeClient.paymentIntents.retrieve(paymentId);
return {
id: paymentIntent.id,
status: paymentIntent.status,
paid: paymentIntent.status === "succeeded",
amount: paymentIntent.amount,
};
},
async handleWebhook(payload, signature) {
const event = stripeClient.webhooks.constructEvent(
payload,
signature,
options.webhookSecret
);
return {
type: event.type,
data: event.data.object,
};
},
};
};
```
**Notice:** Same structure, different API calls!
---
## 🔌 Plugin Comparison
### Better-Auth: Stripe Plugin (for subscriptions)
```typescript
export const stripe = (options: StripeOptions) => {
return {
id: "stripe",
schema: {
subscription: {
fields: {
stripeSubscriptionId: { type: "string" },
stripeCustomerId: { type: "string" },
plan: { type: "string" },
status: { type: "string" },
},
},
},
endpoints: {
"subscription/subscribe": createAuthEndpoint(...),
"subscription/cancel": createAuthEndpoint(...),
},
};
};
```
### Your Library: Subscription Plugin
```typescript
export const subscriptions = (options: SubscriptionOptions) => {
return {
id: "subscriptions",
schema: {
subscription: {
fields: {
paymentProviderId: { type: "string" },
subscriptionId: { type: "string" },
plan: { type: "string" },
status: { type: "string" },
currentPeriodEnd: { type: "date" },
},
},
},
endpoints: {
"subscription/create": createPaymentEndpoint(...),
"subscription/cancel": createPaymentEndpoint(...),
"subscription/upgrade": createPaymentEndpoint(...),
},
};
};
```
**Same plugin architecture!**
---
## ⚙️ Configuration Comparison
### Better-Auth Configuration
```typescript
const auth = betterAuth({
// Configure multiple OAuth providers
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
scopes: ["email", "profile"],
},
github: {
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
},
},
// Add plugins
plugins: [
bearer(),
organization(),
],
});
```
### Your Library Configuration
```typescript
const payments = betterPayments({
// Configure multiple payment providers
providers: {
stripe: {
apiKey: process.env.STRIPE_SECRET_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
},
flutterwave: {
secretKey: process.env.FLUTTERWAVE_SECRET_KEY,
publicKey: process.env.FLUTTERWAVE_PUBLIC_KEY,
},
},
// Add plugins
plugins: [
subscriptions(),
invoicing(),
],
});
```
**Identical configuration pattern!**
---
## 🎯 Usage Comparison
### Better-Auth Usage
```typescript
// Redirect to Google login
const url = await auth.api.signInSocial({
provider: "google",
callbackURL: "/auth/callback",
});
// Verify callback
const session = await auth.api.verifyCallback({
code: request.query.code,
});
// Get user
const user = await auth.api.getSession({
token: session.token,
});
```
### Your Library Usage
```typescript
// Create Stripe payment
const intent = await payments.payment.create("stripe", {
amount: 5000,
currency: "USD",
});
// Redirect user to intent.clientSecret or payment URL
// Verify webhook
const event = await payments.payment.handleWebhook("stripe", {
payload: request.body,
signature: request.headers["stripe-signature"],
});
// Get payment status
const status = await payments.payment.verify("stripe", event.paymentId);
```
**Same API design!**
---
## 🔑 Key Takeaways
| Better-Auth Does | Your Library Does | Same Pattern |
|------------------|-------------------|--------------|
| Abstracts OAuth providers | Abstracts payment gateways | ✅ Provider abstraction |
| Supports Google, GitHub, etc. | Supports Stripe, Flutterwave, etc. | ✅ Multiple providers |
| Plugins add features (SSO, passkey) | Plugins add features (subscriptions, invoicing) | ✅ Plugin system |
| Type-safe configuration | Type-safe configuration | ✅ TypeScript generics |
| Framework agnostic | Framework agnostic | ✅ Works anywhere |
| Monorepo structure | Monorepo structure | ✅ Package organization |
---
## 💡 Aha Moments
### 1. **Provider = Service Abstraction**
Better-auth abstracts OAuth providers (Google, GitHub)
→ You abstract payment providers (Stripe, Flutterwave)
### 2. **Same Flow, Different Domain**
OAuth: redirect → callback → get user
→ Payment: checkout → webhook → verify payment
### 3. **Plugins = Features**
Better-auth plugins: SSO, organizations, passkeys
→ Your plugins: subscriptions, invoicing, split payments
### 4. **Configuration = Developer Experience**
Better-auth makes OAuth simple
→ You make payments simple
---
## 📚 Study Strategy
When reading better-auth code, mentally replace:
- `OAuthProvider` → `PaymentProvider`
- `createAuthorizationURL` → `createPaymentIntent`
- `validateAuthorizationCode` → `verifyPayment`
- `getUserInfo` → `handleWebhook`
- `google()` → `stripe()`
- `github()` → `flutterwave()`
The patterns are **identical**, only the domain changes!
---
## 🎓 Next Steps
1. ✅ Read this mapping document
2. ✅ Study the files in LEARNING_GUIDE.md
3. ✅ Keep this mental mapping in mind
4. ✅ When you see OAuth code, think "How does this apply to payments?"
5. ✅ Start implementing your own providers
**You've got this!** 🚀
+837
View File
@@ -0,0 +1,837 @@
# Quick Start: Building Your Payment Gateway Library
After studying better-auth, here's how to start building your payment gateway abstraction library.
## 🚀 Project Setup
### Step 1: Create Monorepo Structure
```bash
mkdir better-payments
cd better-payments
# Initialize pnpm workspace
pnpm init
# Create packages
mkdir -p packages/core/src/providers
mkdir -p packages/core/src/types
mkdir -p packages/better-payments/src
mkdir -p packages/stripe-plugin/src
mkdir -p examples/nextjs
```
### Step 2: Create `pnpm-workspace.yaml`
```yaml
packages:
- 'packages/*'
- 'examples/*'
```
### Step 3: Create Root `package.json`
```json
{
"name": "better-payments-workspace",
"version": "0.0.1",
"private": true,
"scripts": {
"build": "pnpm -r build",
"dev": "pnpm -r --parallel dev",
"test": "pnpm -r test"
},
"devDependencies": {
"typescript": "^5.3.0",
"tsup": "^8.0.0"
}
}
```
---
## 📝 Step-by-Step Implementation
### Phase 1: Core Types
**File:** `packages/core/src/types/provider.ts`
```typescript
/**
* Core payment provider interface
* Inspired by better-auth's OAuthProvider
*/
export interface PaymentIntent {
id: string;
amount: number;
currency: string;
status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'canceled';
clientSecret?: string;
paymentUrl?: string;
metadata?: Record<string, any>;
}
export interface PaymentStatus {
id: string;
status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'canceled';
amount: number;
currency: string;
paid: boolean;
metadata?: Record<string, any>;
}
export interface WebhookEvent {
type: string;
paymentId: string;
status: PaymentStatus['status'];
data: any;
}
export interface Refund {
id: string;
paymentId: string;
amount: number;
status: 'pending' | 'succeeded' | 'failed';
reason?: string;
}
export interface CreatePaymentData {
amount: number;
currency: string;
customerId?: string;
customerEmail?: string;
description?: string;
metadata?: Record<string, any>;
returnUrl?: string;
}
// Base options that all providers share
export interface ProviderOptions<TProfile = any> {
// Provider credentials
apiKey?: string;
secretKey?: string;
publicKey?: string;
webhookSecret?: string;
// Environment
environment?: 'sandbox' | 'production' | 'test';
// Customization
onPaymentSuccess?: (payment: PaymentStatus) => Promise<void> | void;
onPaymentFailure?: (payment: PaymentStatus) => Promise<void> | void;
mapPaymentData?: (data: TProfile) => Partial<PaymentStatus>;
}
// Main provider interface
export interface PaymentProvider<
TProviderData extends Record<string, any> = Record<string, any>,
TOptions extends Record<string, any> = Partial<ProviderOptions>
> {
id: string; // "stripe", "flutterwave", "paystack"
name: string; // "Stripe", "Flutterwave", "PayStack"
// Required methods
createPaymentIntent: (data: CreatePaymentData) => Promise<PaymentIntent>;
verifyPayment: (paymentId: string) => Promise<PaymentStatus>;
handleWebhook: (payload: any, signature: string) => Promise<WebhookEvent>;
// Optional methods
refundPayment?: (paymentId: string, amount?: number, reason?: string) => Promise<Refund>;
cancelPayment?: (paymentId: string) => Promise<PaymentStatus>;
getPayment?: (paymentId: string) => Promise<PaymentStatus>;
// Configuration
options?: TOptions;
}
export type LiteralString = string & {};
```
---
### Phase 2: First Provider (Stripe)
**File:** `packages/core/src/providers/stripe.ts`
```typescript
import Stripe from 'stripe';
import type { PaymentProvider, ProviderOptions, CreatePaymentData } from '../types/provider.js';
export interface StripeProfile {
id: string;
object: string;
amount: number;
currency: string;
status: string;
// ... add more Stripe-specific fields
}
export interface StripeOptions extends ProviderOptions<StripeProfile> {
apiKey: string;
webhookSecret: string;
apiVersion?: string;
}
export const stripe = (options: StripeOptions) => {
// Initialize Stripe client
const stripeClient = new Stripe(options.apiKey, {
apiVersion: options.apiVersion || '2023-10-16',
});
return {
id: 'stripe' as const,
name: 'Stripe',
async createPaymentIntent(data: CreatePaymentData) {
const paymentIntent = await stripeClient.paymentIntents.create({
amount: data.amount,
currency: data.currency,
customer: data.customerId,
description: data.description,
metadata: data.metadata || {},
// Enable automatic payment methods
automatic_payment_methods: {
enabled: true,
},
});
return {
id: paymentIntent.id,
amount: paymentIntent.amount,
currency: paymentIntent.currency,
status: mapStripeStatus(paymentIntent.status),
clientSecret: paymentIntent.client_secret || undefined,
metadata: paymentIntent.metadata,
};
},
async verifyPayment(paymentId: string) {
const paymentIntent = await stripeClient.paymentIntents.retrieve(paymentId);
const status: PaymentStatus = {
id: paymentIntent.id,
status: mapStripeStatus(paymentIntent.status),
amount: paymentIntent.amount,
currency: paymentIntent.currency,
paid: paymentIntent.status === 'succeeded',
metadata: paymentIntent.metadata,
};
// Call custom handler if provided
if (status.paid && options.onPaymentSuccess) {
await options.onPaymentSuccess(status);
}
return status;
},
async handleWebhook(payload: any, signature: string) {
let event: Stripe.Event;
try {
event = stripeClient.webhooks.constructEvent(
payload,
signature,
options.webhookSecret
);
} catch (err) {
throw new Error(`Webhook signature verification failed: ${err.message}`);
}
const paymentIntent = event.data.object as Stripe.PaymentIntent;
return {
type: event.type,
paymentId: paymentIntent.id,
status: mapStripeStatus(paymentIntent.status),
data: event.data.object,
};
},
async refundPayment(paymentId: string, amount?: number, reason?: string) {
const refund = await stripeClient.refunds.create({
payment_intent: paymentId,
amount,
reason: reason as Stripe.RefundCreateParams.Reason,
});
return {
id: refund.id,
paymentId: paymentId,
amount: refund.amount,
status: refund.status === 'succeeded' ? 'succeeded' :
refund.status === 'pending' ? 'pending' : 'failed',
reason,
};
},
async cancelPayment(paymentId: string) {
const paymentIntent = await stripeClient.paymentIntents.cancel(paymentId);
return {
id: paymentIntent.id,
status: mapStripeStatus(paymentIntent.status),
amount: paymentIntent.amount,
currency: paymentIntent.currency,
paid: false,
};
},
async getPayment(paymentId: string) {
return this.verifyPayment(paymentId);
},
options,
} satisfies PaymentProvider<StripeProfile, StripeOptions>;
};
// Helper function to map Stripe statuses to our standard statuses
function mapStripeStatus(status: string): PaymentStatus['status'] {
switch (status) {
case 'requires_payment_method':
case 'requires_confirmation':
case 'requires_action':
return 'pending';
case 'processing':
return 'processing';
case 'succeeded':
return 'succeeded';
case 'canceled':
return 'canceled';
default:
return 'failed';
}
}
```
---
### Phase 3: Second Provider (Flutterwave)
**File:** `packages/core/src/providers/flutterwave.ts`
```typescript
import type { PaymentProvider, ProviderOptions, CreatePaymentData } from '../types/provider.js';
export interface FlutterwaveProfile {
id: number;
tx_ref: string;
flw_ref: string;
amount: number;
currency: string;
status: string;
// ... add more Flutterwave-specific fields
}
export interface FlutterwaveOptions extends ProviderOptions<FlutterwaveProfile> {
publicKey: string;
secretKey: string;
encryptionKey: string;
environment?: 'sandbox' | 'production';
}
export const flutterwave = (options: FlutterwaveOptions) => {
const baseURL = options.environment === 'production'
? 'https://api.flutterwave.com/v3'
: 'https://api.flutterwave.com/v3'; // Same for both
return {
id: 'flutterwave' as const,
name: 'Flutterwave',
async createPaymentIntent(data: CreatePaymentData) {
const txRef = `tx-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const response = await fetch(`${baseURL}/payments`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${options.secretKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
tx_ref: txRef,
amount: data.amount,
currency: data.currency,
redirect_url: data.returnUrl,
customer: {
email: data.customerEmail,
...(data.customerId && { customer_id: data.customerId }),
},
customizations: {
title: data.description || 'Payment',
},
meta: data.metadata,
}),
});
if (!response.ok) {
throw new Error(`Flutterwave API error: ${response.statusText}`);
}
const result = await response.json();
return {
id: result.data.id.toString(),
amount: data.amount,
currency: data.currency,
status: 'pending',
paymentUrl: result.data.link,
metadata: { tx_ref: txRef, ...data.metadata },
};
},
async verifyPayment(paymentId: string) {
const response = await fetch(
`${baseURL}/transactions/${paymentId}/verify`,
{
headers: {
'Authorization': `Bearer ${options.secretKey}`,
},
}
);
if (!response.ok) {
throw new Error(`Flutterwave verification error: ${response.statusText}`);
}
const result = await response.json();
const data = result.data;
const status: PaymentStatus = {
id: data.id.toString(),
status: mapFlutterwaveStatus(data.status),
amount: data.amount,
currency: data.currency,
paid: data.status === 'successful',
metadata: data.meta,
};
if (status.paid && options.onPaymentSuccess) {
await options.onPaymentSuccess(status);
}
return status;
},
async handleWebhook(payload: any, signature: string) {
// Verify webhook signature
const crypto = await import('crypto');
const hash = crypto
.createHmac('sha256', options.encryptionKey)
.update(JSON.stringify(payload))
.digest('hex');
if (hash !== signature) {
throw new Error('Invalid webhook signature');
}
return {
type: payload.event,
paymentId: payload.data.id.toString(),
status: mapFlutterwaveStatus(payload.data.status),
data: payload.data,
};
},
async refundPayment(paymentId: string, amount?: number) {
const response = await fetch(`${baseURL}/transactions/${paymentId}/refund`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${options.secretKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount,
}),
});
if (!response.ok) {
throw new Error(`Flutterwave refund error: ${response.statusText}`);
}
const result = await response.json();
return {
id: result.data.id.toString(),
paymentId,
amount: result.data.amount,
status: 'succeeded',
};
},
options,
} satisfies PaymentProvider<FlutterwaveProfile, FlutterwaveOptions>;
};
function mapFlutterwaveStatus(status: string): PaymentStatus['status'] {
switch (status) {
case 'successful':
return 'succeeded';
case 'failed':
return 'failed';
case 'pending':
return 'pending';
default:
return 'processing';
}
}
```
---
### Phase 4: Provider Registry
**File:** `packages/core/src/providers/index.ts`
```typescript
import { stripe } from './stripe.js';
import { flutterwave } from './flutterwave.js';
export const paymentProviders = {
stripe,
flutterwave,
// Add more providers here
} as const;
export const paymentProviderList = Object.keys(paymentProviders) as [
'stripe',
...(keyof typeof paymentProviders)[],
];
export type PaymentProviderList = typeof paymentProviderList;
export type PaymentProviders = {
[K in PaymentProviderList[number]]?: Parameters<
(typeof paymentProviders)[K]
>[0] & {
enabled?: boolean;
};
};
// Re-export providers
export { stripe } from './stripe.js';
export { flutterwave } from './flutterwave.js';
export type { StripeOptions } from './stripe.js';
export type { FlutterwaveOptions } from './flutterwave.js';
```
---
### Phase 5: Main Configuration
**File:** `packages/core/src/types/options.ts`
```typescript
import type { PaymentProviders } from '../providers/index.js';
export interface PaymentGatewayOptions {
/**
* Payment providers configuration
*/
providers: PaymentProviders;
/**
* Default currency for payments
* @default "USD"
*/
defaultCurrency?: string;
/**
* Webhook configuration
*/
webhooks?: {
/**
* Path for webhook endpoint
* @default "/api/payments/webhook"
*/
path?: string;
/**
* Handle webhook events
*/
onEvent?: (event: WebhookEvent) => Promise<void> | void;
};
/**
* Plugins
*/
plugins?: PaymentPlugin[];
/**
* Advanced options
*/
advanced?: {
/**
* Retry failed payments automatically
*/
retryFailedPayments?: boolean;
/**
* Auto refund on cancellation
*/
autoRefundOnCancel?: boolean;
/**
* Enable logging
*/
logging?: boolean;
};
}
export interface PaymentPlugin {
id: string;
init?: (context: any) => Promise<void> | void;
endpoints?: Record<string, any>;
}
```
---
### Phase 6: Main Factory Function
**File:** `packages/better-payments/src/index.ts`
```typescript
import type { PaymentGatewayOptions } from '@better-payments/core/types/options';
import { paymentProviders } from '@better-payments/core/providers';
import type { PaymentProvider } from '@better-payments/core/types/provider';
export const betterPayments = <Options extends PaymentGatewayOptions>(
options: Options
) => {
// Instantiate providers from configuration
const providers = Object.entries(options.providers || {})
.map(([key, config]) => {
if (!config || config.enabled === false) {
return null;
}
const providerFactory = paymentProviders[key as keyof typeof paymentProviders];
if (!providerFactory) {
console.warn(`Provider ${key} not found`);
return null;
}
return providerFactory(config as any);
})
.filter((p): p is PaymentProvider => p !== null);
// Helper to find provider
const findProvider = (providerId: string) => {
const provider = providers.find(p => p.id === providerId);
if (!provider) {
throw new Error(`Provider ${providerId} not found or not enabled`);
}
return provider;
};
return {
/**
* Payment operations
*/
payment: {
/**
* Create a new payment
*/
create: async (providerId: string, data: CreatePaymentData) => {
const provider = findProvider(providerId);
return provider.createPaymentIntent({
...data,
currency: data.currency || options.defaultCurrency || 'USD',
});
},
/**
* Verify a payment
*/
verify: async (providerId: string, paymentId: string) => {
const provider = findProvider(providerId);
return provider.verifyPayment(paymentId);
},
/**
* Refund a payment
*/
refund: async (providerId: string, paymentId: string, amount?: number, reason?: string) => {
const provider = findProvider(providerId);
if (!provider.refundPayment) {
throw new Error(`Provider ${providerId} does not support refunds`);
}
return provider.refundPayment(paymentId, amount, reason);
},
/**
* Cancel a payment
*/
cancel: async (providerId: string, paymentId: string) => {
const provider = findProvider(providerId);
if (!provider.cancelPayment) {
throw new Error(`Provider ${providerId} does not support cancellation`);
}
return provider.cancelPayment(paymentId);
},
/**
* Get payment details
*/
get: async (providerId: string, paymentId: string) => {
const provider = findProvider(providerId);
if (provider.getPayment) {
return provider.getPayment(paymentId);
}
return provider.verifyPayment(paymentId);
},
},
/**
* HTTP handler for webhooks
*/
handler: async (request: Request) => {
const url = new URL(request.url);
const webhookPath = options.webhooks?.path || '/api/payments/webhook';
if (!url.pathname.endsWith(webhookPath)) {
return new Response('Not found', { status: 404 });
}
// Extract provider from query or header
const providerId = url.searchParams.get('provider') ||
request.headers.get('x-payment-provider');
if (!providerId) {
return new Response('Provider not specified', { status: 400 });
}
const provider = findProvider(providerId);
// Get signature from headers
const signature = request.headers.get(`${providerId}-signature`) ||
request.headers.get('stripe-signature') ||
request.headers.get('verif-hash') || '';
try {
const payload = await request.text();
const event = await provider.handleWebhook(JSON.parse(payload), signature);
// Call webhook handler
if (options.webhooks?.onEvent) {
await options.webhooks.onEvent(event);
}
return new Response(JSON.stringify({ received: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Webhook error:', error);
return new Response(
JSON.stringify({ error: error.message }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
},
/**
* Configuration
*/
options,
/**
* List of enabled providers
*/
providers: providers.map(p => ({ id: p.id, name: p.name })),
};
};
// Re-export types
export type { PaymentGatewayOptions } from '@better-payments/core/types/options';
export type { PaymentProvider, CreatePaymentData, PaymentIntent, PaymentStatus } from '@better-payments/core/types/provider';
```
---
## 🎯 Usage Example
**File:** `examples/nextjs/app/api/payments/route.ts`
```typescript
import { betterPayments } from 'better-payments';
const payments = betterPayments({
providers: {
stripe: {
apiKey: process.env.STRIPE_SECRET_KEY!,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
onPaymentSuccess: async (payment) => {
console.log('Payment succeeded:', payment.id);
// Send email, update database, etc.
},
},
flutterwave: {
publicKey: process.env.FLUTTERWAVE_PUBLIC_KEY!,
secretKey: process.env.FLUTTERWAVE_SECRET_KEY!,
encryptionKey: process.env.FLUTTERWAVE_ENCRYPTION_KEY!,
},
},
defaultCurrency: 'USD',
});
// Create payment endpoint
export async function POST(request: Request) {
const { provider, amount, currency } = await request.json();
try {
const payment = await payments.payment.create(provider, {
amount,
currency,
customerEmail: 'customer@example.com',
description: 'Test payment',
});
return Response.json(payment);
} catch (error) {
return Response.json({ error: error.message }, { status: 500 });
}
}
// Webhook endpoint
export async function POST_WEBHOOK(request: Request) {
return payments.handler(request);
}
```
---
## ✅ What You've Built
After following this guide, you have:
- ✅ Core provider interface
- ✅ Stripe provider implementation
- ✅ Flutterwave provider implementation
- ✅ Provider registry system
- ✅ Type-safe configuration
- ✅ Main factory function
- ✅ Webhook handling
- ✅ Framework-agnostic API
---
## 🎓 Next Steps
1. **Add more providers** (PayStack, PayPal, Square)
2. **Add plugins** (subscriptions, invoicing, split payments)
3. **Add database support** (save payment records)
4. **Add React hooks** (usePayment, useSubscription)
5. **Add documentation** (README, API reference)
6. **Add tests** (unit tests, integration tests)
7. **Publish to npm** (better-payments)
---
**You've got the foundation! Start building!** 🚀
+206
View File
@@ -0,0 +1,206 @@
# Better-Auth Study Checklist
Use this checklist to track your progress as you study the better-auth codebase.
## 📋 Phase 1: Provider Abstraction
- [ ] **File 1:** `packages/core/src/oauth2/oauth-provider.ts` (Lines 14-83)
- [ ] Understand `OAuthProvider` interface
- [ ] Note required methods
- [ ] Note optional methods
- [ ] Understand generic types `<T, O>`
- [ ] **File 2:** `packages/core/src/social-providers/github.ts` (Lines 1-200)
- [ ] Understand `GithubProfile` interface
- [ ] Understand `GithubOptions` interface
- [ ] See how `github()` factory works
- [ ] Note API endpoint patterns
- [ ] **File 3:** `packages/core/src/social-providers/google.ts` (Lines 1-250)
- [ ] Compare with GitHub provider
- [ ] Understand additional features (verifyIdToken)
- [ ] Note token refresh pattern
- [ ] See helper function usage
- [ ] **File 4:** `packages/core/src/social-providers/paypal.ts` (Lines 1-150)
- [ ] Understand environment handling (sandbox/production)
- [ ] Note conditional endpoint URLs
- [ ] See how it differs from GitHub/Google
- [ ] **File 5:** `packages/core/src/social-providers/index.ts` (Lines 1-100)
- [ ] Understand `socialProviders` registry
- [ ] Understand `socialProviderList` enum
- [ ] Understand `SocialProviders` type
- [ ] Note how types are generated
**Exercise after Phase 1:**
- [ ] Design your `PaymentProvider` interface (on paper or in code)
- [ ] List required methods for payment providers
- [ ] List optional methods for payment providers
---
## 📋 Phase 2: Configuration System
- [ ] **File 6:** `packages/core/src/types/init-options.ts` (Lines 1-300)
- [ ] Understand `BetterAuthOptions` structure
- [ ] Note how `socialProviders` is configured
- [ ] Note how plugins array works
- [ ] See `emailAndPassword` config pattern
- [ ] **File 7:** `packages/core/src/oauth2/oauth-provider.ts` (Lines 85-120)
- [ ] Understand `ProviderOptions<Profile>` interface
- [ ] Note common fields (clientId, clientSecret)
- [ ] Note customization options (getUserInfo, mapProfileToUser)
**Exercise after Phase 2:**
- [ ] Design your `PaymentGatewayOptions` interface
- [ ] Design your `ProviderOptions` base interface
- [ ] Design provider-specific options (StripeOptions, FlutterwaveOptions)
---
## 📋 Phase 3: Core Factory
- [ ] **File 8:** `packages/better-auth/src/auth/auth.ts` (Lines 1-50)
- [ ] Understand `betterAuth()` function
- [ ] Note how generics are used
- [ ] See delegation to `createBetterAuth`
- [ ] **File 9:** `packages/better-auth/src/auth/base.ts` (Lines 1-150)
- [ ] Understand `createBetterAuth()` factory
- [ ] Note the `handler` function
- [ ] See plugin aggregation pattern
- [ ] Understand error code collection
- [ ] **File 10:** `packages/better-auth/src/context/create-context.ts` (Lines 1-300)
- [ ] See how providers are instantiated from config
- [ ] Note provider validation
- [ ] Understand context object structure
**Exercise after Phase 3:**
- [ ] Sketch out your `betterPayments()` function
- [ ] Sketch out your `createPaymentGateway()` factory
- [ ] Design your context object structure
---
## 📋 Phase 4: Plugin System
- [ ] **File 11:** `packages/core/src/types/plugin.ts` (Lines 1-200)
- [ ] Understand `BetterAuthPlugin` interface
- [ ] Note lifecycle hooks (init, before, after)
- [ ] Understand schema definition
- [ ] Note endpoints pattern
- [ ] **File 12:** `packages/better-auth/src/plugins/bearer/index.ts` (Lines 1-200)
- [ ] Understand bearer plugin pattern
- [ ] See hooks.before and hooks.after
- [ ] Note matcher function
- [ ] Understand header manipulation
- [ ] **File 13:** `packages/stripe/src/index.ts` (Lines 1-500)
- [ ] See complex plugin example
- [ ] Note schema definition for subscriptions
- [ ] Note multiple endpoints pattern
- [ ] See Stripe SDK integration
- [ ] Note error codes definition
- [ ] **File 14:** `packages/better-auth/src/context/helpers.ts` (Lines 1-150)
- [ ] Understand `runPluginInit()` function
- [ ] See how plugins modify options
- [ ] Note deep merge pattern
**Exercise after Phase 4:**
- [ ] Design your `PaymentPlugin` interface
- [ ] Sketch a subscriptions plugin
- [ ] Sketch an invoicing plugin
---
## 📋 Phase 5: Package Structure
- [ ] **File 15:** `pnpm-workspace.yaml`
- [ ] Understand monorepo structure
- [ ] Note package organization
- [ ] **File 16:** `packages/core/package.json`
- [ ] See exports configuration
- [ ] Note dependencies structure
- [ ] **File 17:** `packages/better-auth/package.json`
- [ ] See subpath exports
- [ ] Note plugin export pattern
**Exercise after Phase 5:**
- [ ] Design your monorepo structure
- [ ] Plan your package names
- [ ] Decide on export strategy
---
## 🎯 Final Exercises
- [ ] **Exercise 1:** Implement a basic Stripe provider
- [ ] Create `StripeOptions` interface
- [ ] Create `stripe()` factory function
- [ ] Implement `createPaymentIntent()`
- [ ] Implement `verifyPayment()`
- [ ] Implement `handleWebhook()`
- [ ] **Exercise 2:** Implement a basic Flutterwave provider
- [ ] Create `FlutterwaveOptions` interface
- [ ] Create `flutterwave()` factory function
- [ ] Implement core methods
- [ ] Handle sandbox/production environments
- [ ] **Exercise 3:** Create provider registry
- [ ] Create `paymentProviders` object
- [ ] Create `PaymentProviders` type
- [ ] Test type safety
- [ ] **Exercise 4:** Build minimal working version
- [ ] Create `betterPayments()` function
- [ ] Support 2 providers (Stripe + Flutterwave)
- [ ] Test with actual API calls (sandbox mode)
---
## 📊 Progress Tracker
**Phase 1:** ☐☐☐☐☐ (0/5 files)
**Phase 2:** ☐☐ (0/2 files)
**Phase 3:** ☐☐☐ (0/3 files)
**Phase 4:** ☐☐☐☐ (0/4 files)
**Phase 5:** ☐☐☐ (0/3 files)
**Exercises:** ☐☐☐☐ (0/4 completed)
**Overall Progress:** 0% (0/21 total tasks)
---
## 📝 Notes Section
Use this space to write down:
- Questions you have
- Aha moments
- Patterns you notice
- Ideas for your implementation
```
[Your notes here]
```
---
## 🎓 Completion
When you check all boxes above, you'll be ready to:
✅ Build your payment gateway abstraction library
✅ Support multiple payment providers
✅ Create a plugin system
✅ Structure a professional monorepo
✅ Provide excellent TypeScript types
**Good luck!** 🚀