Payment System (Subscription Management)
Status: ✅ Complete (Mock Implementation) - Production payment provider (Stripe) deferred until after testing
Last Updated: 2026-01-26
Overview
The payment system handles subscription management for ARC // OS. It uses an adapter pattern (similar to email service and file storage) to allow easy switching between payment providers.
Design Principle: Interface-based design allows swapping payment providers (mock → Stripe → future providers) without code changes.
Philosophy
- Free Mode: Full access to core features—no limits, no data selling
- Subscribed Mode: Enhanced features + support ongoing development
- Privacy Promise: We never sell your data, pictures, or personal information—regardless of subscription status
- Transparent: Subscription helps maintain the platform, not monetize user data
Subscription Tiers
Free Mode (Default):
- ✅ All core features (daily planning, tasks, training, nutrition, sleep, journaling, etc.)
- ✅ Core gamification (XP, streaks, basic achievements, daily battles, shop)
- Note: Core gamification remains free as it's an important driver of daily usage
- ✅ Social features (friends, messaging)
- ✅ Basic integrations (Google Fit - if implemented)
- ✅ All data is yours—no limits
- ✅ No data selling—ever
Subscribed Mode:
- ✅ Everything in Free Mode, plus:
- ✅ Enhanced gamification features:
- Advanced battle features (weekly boss prep insights, advanced battle analytics)
- Premium skill tree nodes
- Exclusive gear sets and items
- Enhanced idle progression bonuses
- Advanced collection features
- ✅ Developer/Integration features:
- Outgoing webhooks (trigger events to external services)
- Advanced integrations (Strava, Zwift, and other niche products)
- Custom API access (if needed for advanced use cases)
- Integration management UI
- ✅ Advanced features:
- Advanced analytics and insights
- Priority support
- Early access to new features
- Enhanced customization options
- Additional storage for files and photos
- Advanced export/import features
Design Principles
- No aggressive gating - Core features remain free
- Gamification balance - Core gamification (XP, streaks, daily battles) stays free as it drives daily usage; enhanced features can be subscription-only
- Value-driven - Subscribed features provide clear value (webhooks, integrations, advanced analytics)
- Transparent - Clear what's free vs subscribed
- Privacy-first - Subscription doesn't change privacy promise
- User-friendly - Easy to upgrade, easy to cancel
- Integration strategy - Basic integrations (Google Fit) free; niche products (Strava, Zwift) subscription-only
Architecture
Adapter Pattern
IPaymentAdapter (interface)
├── MockPaymentAdapter (development/testing)
└── StripePaymentAdapter (production - future)
Key Benefits:
- Easy testing with mock adapter
- Simple provider switching via environment variable
- Consistent interface across all providers
- No code changes needed when switching providers
Interface Design
IPaymentAdapter Interface
/**
* Payment adapter interface
* Allows easy swapping between mock, Stripe, and future payment providers
*/
export interface IPaymentAdapter {
/**
* Create a subscription checkout session
* @param userId - User ID
* @param planId - Subscription plan ID
* @param successUrl - URL to redirect after successful payment
* @param cancelUrl - URL to redirect after cancelled payment
* @returns Checkout session URL
*/
createCheckoutSession(
userId: string,
planId: string,
successUrl: string,
cancelUrl: string
): Promise<string>;
/**
* Create a customer portal session
* @param userId - User ID
* @param returnUrl - URL to return to after portal session
* @returns Portal session URL
*/
createPortalSession(userId: string, returnUrl: string): Promise<string>;
/**
* Handle webhook event from payment provider
* @param payload - Raw webhook payload
* @param signature - Webhook signature for verification
* @returns Processed webhook event
*/
handleWebhook(payload: string, signature: string): Promise<WebhookEvent>;
/**
* Get subscription details
* @param subscriptionId - Subscription ID from payment provider
* @returns Subscription details
*/
getSubscription(subscriptionId: string): Promise<SubscriptionDetails>;
/**
* Cancel a subscription
* @param subscriptionId - Subscription ID from payment provider
* @returns Cancelled subscription details
*/
cancelSubscription(subscriptionId: string): Promise<SubscriptionDetails>;
/**
* Update subscription (change plan, etc.)
* @param subscriptionId - Subscription ID from payment provider
* @param updates - Subscription updates
* @returns Updated subscription details
*/
updateSubscription(
subscriptionId: string,
updates: SubscriptionUpdates
): Promise<SubscriptionDetails>;
}
Types
/**
* Webhook event types
*/
export type WebhookEventType =
| 'checkout.session.completed'
| 'customer.subscription.created'
| 'customer.subscription.updated'
| 'customer.subscription.deleted'
| 'invoice.payment_succeeded'
| 'invoice.payment_failed';
/**
* Webhook event
*/
export interface WebhookEvent {
type: WebhookEventType;
subscriptionId?: string;
userId: string;
metadata: Record<string, unknown>;
timestamp: Date;
}
/**
* Subscription details
*/
export interface SubscriptionDetails {
id: string;
userId: string;
status: 'active' | 'canceled' | 'past_due' | 'unpaid' | 'trialing';
planId: string;
currentPeriodStart: Date;
currentPeriodEnd: Date;
cancelAtPeriodEnd: boolean;
metadata: Record<string, unknown>;
}
/**
* Subscription updates
*/
export interface SubscriptionUpdates {
planId?: string;
cancelAtPeriodEnd?: boolean;
metadata?: Record<string, unknown>;
}
Implementations
MockPaymentAdapter (Development/Testing)
Purpose: Mock implementation for development and testing.
Features:
- Simulates payment flow without real payments
- Stores subscription data in memory/database
- Provides test endpoints for webhook simulation
- No external API calls
Usage:
// Automatically used when PAYMENT_PROVIDER=mock
const adapter = getPaymentAdapter();
const checkoutUrl = await adapter.createCheckoutSession(
userId,
'subscribed',
'https://app.arcos.com/success',
'https://app.arcos.com/cancel'
);
Test Endpoints:
POST /api/payments/test/checkout- Simulate successful checkoutPOST /api/payments/test/webhook- Simulate webhook eventsGET /api/payments/test/subscriptions- View mock subscriptions
StripePaymentAdapter (Production - Future)
Purpose: Production Stripe integration.
Features:
- Real payment processing via Stripe
- Secure webhook verification
- Customer portal integration
- Subscription management
Setup:
- Create Stripe account
- Get API keys (publishable and secret)
- Configure webhook endpoint
- Set environment variables
Environment Variables:
PAYMENT_PROVIDER=stripe
STRIPE_SECRET_KEY=sk_live_...
STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
Service Layer
PaymentService
Purpose: Business logic layer for payment operations.
Methods:
createSubscription(userId, planId)- Create subscription checkouthandleWebhookEvent(event)- Process webhook eventsgetUserSubscription(userId)- Get user's current subscriptioncancelSubscription(userId)- Cancel user's subscriptionupdateSubscription(userId, updates)- Update subscriptionverifySubscriptionAccess(userId, feature)- Check feature access
Integration:
- Uses
IPaymentAdapterfor payment provider operations - Manages
Subscriptiondatabase records - Handles subscription state transitions
- Enforces business rules (grace periods, etc.)
Database Schema
model Subscription {
id String @id @default(uuid())
userId String
provider String // 'stripe', 'mock', etc.
providerSubscriptionId String // Subscription ID from payment provider
planId String // 'free', 'subscribed'
status String // 'active', 'canceled', 'past_due', 'unpaid', 'trialing'
currentPeriodStart DateTime
currentPeriodEnd DateTime
cancelAtPeriodEnd Boolean @default(false)
canceledAt DateTime?
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId]) // One subscription per user
@@index([userId])
@@index([providerSubscriptionId])
@@index([status])
@@map("subscriptions")
}
API Endpoints
Subscription Management
GET /api/subscription- Get current subscriptionPOST /api/subscription/checkout- Create checkout sessionPOST /api/subscription/cancel- Cancel subscriptionPOST /api/subscription/update- Update subscriptionGET /api/subscription/portal- Get customer portal URL
Webhooks
POST /api/subscription/webhook- Payment provider webhook endpoint
Testing (Mock Only)
POST /api/payments/test/checkout- Simulate successful checkoutPOST /api/payments/test/webhook- Simulate webhook eventsGET /api/payments/test/subscriptions- View mock subscriptions
Implementation Status
Phase 1: Mock Adapter (Development) ✅ Complete
- ✅ Create
IPaymentAdapterinterface - Complete: Createdpayment-adapter.interface.tswith full interface definition - ✅ Implement
MockPaymentAdapter- Complete: Full implementation with mock checkout, cancellation, webhook handling, and invoice generation - ✅ Create
SubscriptionServicewith business logic - Complete: Full service with CRUD operations, feature gating, and subscription management - ✅ Database schema:
Subscriptiontable - Complete: Added Subscription model to Prisma schema with all required fields - ✅ API routes for subscription management - Complete: Full API routes (GET
/subscription, POST/subscription/upgrade, POST/subscription/cancel, GET/subscription/features, POST/subscription/webhook, POST/subscription/admin/grant) - ✅ Admin endpoint for testing - Complete: POST
/subscription/admin/grantendpoint for admin users to manually grant subscriptions - ✅ Comprehensive test coverage - Complete: 28 service tests, route tests, and component tests all passing
Phase 2: Stripe Integration (Production) ⏳ Deferred
- ⏳ Implement
StripePaymentAdapter- Deferred until after testing - ⏳ Stripe webhook verification - Deferred until after testing
- ⏳ Customer portal integration - Deferred until after testing
- ⏳ Subscription plan configuration - Deferred until after testing
- ⏳ Production testing - Deferred until after testing
- ⏳ Migration from mock to Stripe - Deferred until after testing
Phase 3: Frontend Integration ✅ Complete
- ✅ Subscription status display - Complete: Added subscription section to SettingsPage with plan, status, and period information
- ✅ Checkout flow UI - Complete: Upgrade button triggers checkout flow, handles success/cancel pages
- ✅ Subscription management UI - Complete: Full SubscriptionPage with current tier display, upgrade button, cancel subscription flow, and mock payment method display
- ✅ Feature gating UI - Complete: Implemented reusable components (LockedFeatureBadge, LockedFeaturePrompt, UpgradeCTA) for feature gating with graceful degradation
- ✅ Feature comparison page - Complete: Integrated into SubscriptionPage with feature list showing available/locked features
- ✅ Comprehensive tests - Complete: Added comprehensive tests for useSubscription hook (5 tests) and SubscriptionPage component (8 tests), all passing
Environment Variables
# Payment Provider
PAYMENT_PROVIDER=mock # 'mock' or 'stripe'
# Stripe (when PAYMENT_PROVIDER=stripe)
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Subscription Plans
SUBSCRIPTION_PLAN_ID=price_... # Stripe price ID
Testing
Mock Adapter Testing
describe('PaymentService with MockAdapter', () => {
it('should create checkout session', async () => {
const checkoutUrl = await paymentService.createSubscription(
userId,
'subscribed'
);
expect(checkoutUrl).toContain('/checkout');
});
it('should handle webhook events', async () => {
const event = await paymentService.handleWebhookEvent({
type: 'checkout.session.completed',
userId,
subscriptionId: 'sub_123',
});
expect(event).toBeDefined();
});
});
Integration Testing
- Test checkout flow end-to-end
- Test webhook processing
- Test subscription state transitions
- Test feature gating
- Test cancellation flow
Security Considerations
Webhook Verification
- Stripe: Verify webhook signatures using
STRIPE_WEBHOOK_SECRET - Mock: Skip verification in development, but validate event structure
Payment Data
- Never store credit card information
- Store only subscription IDs and metadata
- Use payment provider's secure tokenization
User Ownership
- Always verify user owns subscription before operations
- Enforce user isolation in all payment operations
- Validate userId matches authenticated user
Future Enhancements
- Multiple payment providers - PayPal, Apple Pay, Google Pay
- Subscription tiers - Multiple plan options
- Promo codes - Discount codes and coupons
- Gift subscriptions - Gift subscriptions to friends
- Usage-based billing - For future features
- Enterprise plans - Team/organization subscriptions
Related Documentation
- Subscription System: See
docs/project/TODO.mdfor subscription feature planning - Adapter Pattern: Follows same pattern as
EmailServiceandFileStorageAdapter - Database Schema: See Prisma schema for
Subscriptionmodel
Current Status: Mock implementation complete and fully functional for test deployment. Real payment provider (Stripe) integration deferred until after testing phase.