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!** 🚀