Skip to main content

Payment Architecture

The Ever Works template implements a provider-agnostic payment system that supports four payment providers: Stripe, LemonSqueezy, Polar, and Solidgate. The architecture uses a factory pattern with singleton provider instances, allowing runtime provider switching.

Source Locations

lib/payment/lib/payment-provider-factory.ts     # Factory class
lib/payment/lib/payment-service.ts # Service facade
lib/payment/lib/payment-service-manager.ts # Singleton manager with provider switching
lib/payment/config/payment-provider-manager.ts # Config validation & provider instantiation
lib/payment/types/payment-types.ts # Shared interfaces and types
lib/payment/lib/providers/ # Provider implementations
stripe-provider.ts
solidgate-provider.ts
lemonsqueezy-provider.ts
polar-provider.ts

System Diagram

+------------------+      +------------------------+
| React Component | ---> | PaymentServiceManager |
+------------------+ | (singleton) |
+------------------------+
|
+------------------------+
| PaymentService |
| (facade) |
+------------------------+
|
+------------------------+
| PaymentProviderFactory |
+------------------------+
/ | | \
Stripe Solidgate Lemon Polar
Provider Provider Squeezy Provider
Provider

Provider Interface

Every payment provider implements the same PaymentProviderInterface:

interface PaymentProviderInterface {
// Payment operations
createPaymentIntent(params: CreatePaymentParams): Promise<PaymentIntent>;
confirmPayment(paymentId: string, paymentMethodId: string): Promise<PaymentIntent>;
verifyPayment(paymentId: string): Promise<PaymentVerificationResult>;
createSetupIntent(user: User | null): Promise<SetupIntent>;

// Customer management
createCustomer(params: CreateCustomerParams): Promise<CustomerResult>;
hasCustomerId(user: User | null): boolean;
getCustomerId(user: User | null): Promise<string | null>;

// Subscription management
createSubscription(params: CreateSubscriptionParams): Promise<SubscriptionInfo>;
cancelSubscription(subscriptionId: string, cancelAtPeriodEnd?: boolean): Promise<SubscriptionInfo>;
updateSubscription(params: UpdateSubscriptionParams): Promise<SubscriptionInfo>;

// Webhooks
handleWebhook(
payload: any, signature: string, rawBody?: string,
timestamp?: string, webhookId?: string
): Promise<WebhookResult>;

// Refunds
refundPayment(paymentId: string, amount?: number): Promise<any>;

// Client-side configuration
getClientConfig(): ClientConfig;
getUIComponents(): UIComponents;
}

This interface ensures that switching providers requires zero changes to calling code.

PaymentProviderFactory

The factory creates provider instances based on a provider type string:

export type SupportedProvider = 'stripe' | 'solidgate' | 'lemonsqueezy' | 'polar';

export class PaymentProviderFactory {
static createProvider(
providerType: SupportedProvider,
config: PaymentProviderConfig
): PaymentProviderInterface {
switch (providerType) {
case 'stripe':
return new StripeProvider(config);
case 'solidgate':
return new SolidgateProvider(config);
case 'lemonsqueezy':
return new LemonSqueezyProvider(config);
case 'polar':
return new PolarProvider(config);
default:
throw new Error(`Unsupported payment provider: ${providerType}`);
}
}
}

PaymentService (Facade)

The PaymentService wraps a single provider instance behind a clean API. It delegates every call to the underlying provider:

export class PaymentService {
private provider: PaymentProviderInterface;

constructor(config: PaymentServiceConfig) {
this.provider = PaymentProviderFactory.createProvider(
config.provider, config.config
);
}

async createPaymentIntent(params: CreatePaymentParams): Promise<PaymentIntent> {
return this.provider.createPaymentIntent(params);
}

async createSubscription(params: CreateSubscriptionParams): Promise<SubscriptionInfo> {
return this.provider.createSubscription(params);
}

getUIComponents(): UIComponents {
return this.provider.getUIComponents();
}

// ... all other interface methods delegated
}

Plan Definitions

The service also defines payment plan constants:

enum PaymentPlanId {
FREE = "1",
ONE_TIME = "2",
SUBSCRIPTION = "3",
PREMIUM = "4",
}

interface PaymentPlan {
id: PaymentPlanId;
amount: number;
isSubscription: boolean;
features: string[];
}

PaymentServiceManager

The manager handles singleton lifecycle and runtime provider switching. It persists the selected provider in localStorage:

export class PaymentServiceManager {
private static instance: PaymentServiceManager;
private currentService: PaymentService | null = null;
private readonly STORAGE_KEY = 'everworks_template.payment_provider.selected';
private readonly DEFAULT_PROVIDER: SupportedProvider;

static getInstance(
providerConfigs: Record<SupportedProvider, PaymentProviderConfig>,
defaultProvider?: SupportedProvider
): PaymentServiceManager {
if (!PaymentServiceManager.instance) {
PaymentServiceManager.instance = new PaymentServiceManager(
providerConfigs, defaultProvider
);
}
return PaymentServiceManager.instance;
}

getPaymentService(): PaymentService {
if (!this.currentService) {
const provider = this.getStoredProvider();
this.currentService = new PaymentService({
provider,
config: this.providerConfigs[provider],
});
}
return this.currentService;
}

async switchProvider(newProvider: SupportedProvider): Promise<void> {
if (this.getStoredProvider() !== newProvider) {
this.setStoredProvider(newProvider);
this.currentService = new PaymentService({
provider: newProvider,
config: this.providerConfigs[newProvider],
});
}
}

getCurrentProvider(): SupportedProvider { /* ... */ }
getAvailableProviders(): SupportedProvider[] { /* ... */ }
}

SSR Safety

The manager handles server-side rendering by defaulting to the configured default provider when localStorage is unavailable:

private getStoredProvider(): SupportedProvider {
if (typeof window === 'undefined') return this.DEFAULT_PROVIDER;
const stored = localStorage.getItem(this.STORAGE_KEY);
return (stored as SupportedProvider) || this.DEFAULT_PROVIDER;
}

PaymentProviderManager (Config Layer)

The PaymentProviderManager provides an alternative singleton access pattern with per-provider lazy initialization and configuration validation:

export class PaymentProviderManager {
private static instances = new Map<string, any>();

static getStripeProvider(): StripeProvider { /* ... */ }
static getLemonsqueezyProvider(): LemonSqueezyProvider { /* ... */ }
static getPolarProvider(): PolarProvider { /* ... */ }
static getSolidgateProvider(): SolidgateProvider { /* ... */ }

static reset(): void {
this.instances.clear();
}

static isInitialized(providerName: string): boolean {
return this.instances.has(providerName);
}
}

Convenience Functions

The config module exports helper functions for quick access to providers:

// Get or create (lazy init)
getOrCreateStripeProvider(): StripeProvider
getOrCreateLemonsqueezyProvider(): LemonSqueezyProvider
getOrCreatePolarProvider(): PolarProvider
getOrCreateSolidgateProvider(): SolidgateProvider

// Generic accessor
getOrCreateProvider(providerName: string): PaymentProviderInterface

// Reset all singletons
resetPaymentProviders(): void

ConfigManager

The internal ConfigManager class loads and validates environment variables for all providers. Each provider's config is validated on first access, not at startup:

// Solidgate config shape
{
apiKey: process.env.SOLIDGATE_API_KEY,
secretKey: process.env.SOLIDGATE_SECRET_KEY,
webhookSecret: process.env.SOLIDGATE_WEBHOOK_SECRET,
options: {
publishableKey: process.env.NEXT_PUBLIC_SOLIDGATE_PUBLISHABLE_KEY,
merchantId: process.env.SOLIDGATE_MERCHANT_ID,
apiBaseUrl: process.env.SOLIDGATE_API_BASE_URL || 'https://api.solidgate.com/v1',
}
}

Validation is triggered once per provider and raises descriptive errors listing the missing variables.

Shared Types

PaymentIntent

interface PaymentIntent {
id: string;
amount: number;
currency: string;
status: string;
clientSecret?: string;
customerId?: string;
}

SubscriptionInfo

interface SubscriptionInfo {
id: string;
customerId: string;
status: SubscriptionStatus;
currentPeriodEnd?: number | null;
cancelAtPeriodEnd?: boolean;
cancelAt?: number | null;
trialEnd?: number | null;
priceId: string;
}

ClientConfig

interface ClientConfig {
publicKey: string;
paymentGateway: 'stripe' | 'solidgate' | 'lemonsqueezy' | 'polar';
options?: Record<string, any>;
}

UIComponents

Each provider returns a set of UI components for rendering payment forms:

interface UIComponents {
PaymentForm: React.ComponentType<PaymentFormProps>;
logo: string;
cardBrands: CardBrandIcon[];
supportedPaymentMethods: string[];
translations: Record<string, Record<string, string>>;
}

Adding a New Provider

To add a fifth payment provider, follow these steps:

1. Create the Provider Class

// lib/payment/lib/providers/newprovider-provider.ts
export class NewProvider implements PaymentProviderInterface {
constructor(config: PaymentProviderConfig) {
// Initialize with API keys from config
}

// Implement all interface methods
async createPaymentIntent(params) { /* ... */ }
async createCustomer(params) { /* ... */ }
// ... etc.
}

2. Register in the Factory

// lib/payment/lib/payment-provider-factory.ts
export type SupportedProvider =
'stripe' | 'solidgate' | 'lemonsqueezy' | 'polar' | 'newprovider';

static createProvider(providerType, config) {
switch (providerType) {
// ... existing cases
case 'newprovider':
return new NewProvider(config);
}
}

3. Add Configuration

// lib/payment/config/payment-provider-manager.ts
// Add to ConfigManager:
private static newproviderApiKey = process.env.NEWPROVIDER_API_KEY || '';

// Add validation method
private static validateNewProviderConfig(): void { /* ... */ }

// Add to PaymentProviderManager
static getNewProvider(): NewProvider { /* ... */ }

4. Create Webhook Route

// app/api/newprovider/webhook/route.ts
export async function POST(request: NextRequest) {
// Verify signature, parse events, delegate to WebhookSubscriptionService
}

5. Create UI Component

// lib/payment/ui/newprovider/newprovider-elements.tsx
export default function NewProviderElements(props: PaymentFormProps) {
// Render the provider's payment form
}

Utility Functions

The payment types module includes formatting helpers:

// Convert cents to formatted currency string
formatCentsToCurrency(2999, 'USD', 'en-US') // "$29.99"

// Convert between cents and decimals
convertCentsToDecimal(2999) // 29.99
convertDecimalToCents(29.99) // 2999

// Timestamp conversions
convertNumberToDate(1640995200) // Date object
safeTimestampToDate(timestamp) // Date | undefined (handles null/NaN)