Source:
ocean/docs/adr/0012-graphql-billing-architecture.md| ✏️ Edit on GitHub
ADR-0012: GraphQL-Based Billing Architecture
Status
Accepted
Context
The application requires a secure, scalable billing system integrated with Stripe. Initial implementation used direct Stripe API calls from Edge Functions, but this approach had several limitations:
- Security concerns: Direct client-to-Stripe communication exposes implementation details
- Consistency: No single source of truth for billing state
- Performance: Multiple API calls to Stripe for each page load
- Flexibility: Difficult to switch payment providers or add custom billing logic
Decision
We will implement a GraphQL-based billing architecture that:
- Uses GraphQL as the single API layer for all billing operations
- Caches Stripe data in the master database for performance and reliability
- Implements billing operations through GraphQL resolvers with proper authorization
- Syncs Stripe data periodically to maintain consistency
- Uses PostHog feature flags to control billing features
Architecture
Data Flow
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │────▶│ GraphQL │────▶│ Master │
│ (React) │ │ Yoga │ │ Database │
└─────────────┘ └──────────────┘ └─────────────┘
│ │
▼ ▼
┌──────────────┐ ┌─────────────┐
│ Stripe │────▶│ Sync │
│ API │ │ Worker │
└──────────────┘ └─────────────┘
Database Schema
-- Products and Pricing
stripe_products (
id, name, description, features[], metadata
)
stripe_prices (
id, product_id, currency, unit_amount, recurring
)
-- Subscriptions
subscription_items (
id, subscription_id, price_id, quantity
)
-- Payment Methods
payment_methods (
id, customer_id, type, card_details, is_default
)
-- Organizations (extended)
organizations (
+ billing_email, tax_id, billing_address
+ subscription details, trial dates
+ default_payment_method_id
)
GraphQL Schema
type Query {
availableProducts: [Product!]!
billingDetails: BillingDetails!
paymentMethods: [PaymentMethod!]!
invoices(limit: Int, offset: Int): InvoiceConnection!
canManageBilling: Boolean!
}
type Mutation {
createSubscription(input: CreateSubscriptionInput!): SubscriptionResult!
updateSubscription(input: UpdateSubscriptionInput!): SubscriptionResult!
cancelSubscription(immediately: Boolean): SubscriptionResult!
createSetupIntent: SetupIntentResult!
setDefaultPaymentMethod(paymentMethodId: String!): PaymentMethod!
createCustomerPortalSession(returnUrl: String!): PortalSessionResult!
}
Implementation Details
1. GraphQL Resolvers
- All billing operations go through GraphQL resolvers
- Resolvers check organization membership and role
- Operations are logged for audit trail
- Caching with Redis/in-memory fallback
2. Stripe Integration
- Server-side only via GraphQL resolvers
- Webhook handler updates database state
- Sync function refreshes product/price catalog
- Customer portal for advanced operations
3. Security Model
// Authorization checks in resolvers
const membership = await context.getMembership(organization.id)
if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) {
throw new Error('Insufficient permissions')
}
4. Client-Side Hooks
// Clean API for React components
const { data: products } = useProducts()
const { data: billing } = useBillingDetails()
const createSubscription = useCreateSubscription()
5. Feature Flags
// PostHog integration for gradual rollout
const embedPaymentsEnabled = usePostHogFeature('embed-payments')
const advancedBillingEnabled = usePostHogFeature('advanced-billing')
Security Considerations
1. API Security
- All operations require authentication
- Role-based access control (owner/admin only)
- No direct Stripe keys in client code
- GraphQL resolvers validate all inputs
2. Data Security
- Sensitive data stored in master database only
- Payment methods show only last 4 digits
- No full card numbers stored
- Audit trail for all billing changes
3. Network Security
- All API calls over HTTPS
- CORS configured for allowed origins
- Rate limiting on GraphQL endpoint
- Webhook signature verification
Migration Strategy
Phase 1: Database Setup ✓
- Create billing tables
- Add organization billing fields
- Set up RLS policies
Phase 2: GraphQL Implementation ✓
- Create schema and types
- Implement resolvers
- Set up caching layer
Phase 3: Client Migration ✓
- Create GraphQL-based hooks
- Update components to use new hooks
- Remove direct Stripe API calls
Phase 4: Data Sync
- Implement webhook handlers
- Create sync workers
- Set up monitoring
Consequences
Positive
- Single source of truth: Database is authoritative for billing state
- Better performance: Cached data reduces Stripe API calls
- Improved security: No client-side Stripe operations
- Flexibility: Easy to add custom billing logic or switch providers
- Consistency: All billing operations follow same pattern
- Auditability: Complete trail of billing changes
Negative
- Complexity: More moving parts than direct Stripe integration
- Sync lag: Database may be slightly out of sync with Stripe
- Maintenance: Need to maintain sync logic and handle edge cases
Neutral
- Testing: Requires mocking GraphQL layer for tests
- Monitoring: Need to monitor both database and Stripe state
- Documentation: Team needs to understand GraphQL patterns
Alternatives Considered
1. Direct Stripe API from Client
- Rejected: Security concerns, exposes implementation details
2. REST API Layer
- Rejected: GraphQL provides better type safety and flexibility
3. Stripe Elements Only
- Rejected: Limited customization, doesn't solve data consistency
4. Third-party Billing Service
- Rejected: Adds dependency, less control over billing logic
References
Related ADRs
- ADR-0003: GraphQL Schema-First TypeScript
- ADR-0006: Fail-Closed Authentication
- ADR-0011: Comprehensive Design System Enhancements