Skip to main content

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:

  1. Security concerns: Direct client-to-Stripe communication exposes implementation details
  2. Consistency: No single source of truth for billing state
  3. Performance: Multiple API calls to Stripe for each page load
  4. Flexibility: Difficult to switch payment providers or add custom billing logic

Decision

We will implement a GraphQL-based billing architecture that:

  1. Uses GraphQL as the single API layer for all billing operations
  2. Caches Stripe data in the master database for performance and reliability
  3. Implements billing operations through GraphQL resolvers with proper authorization
  4. Syncs Stripe data periodically to maintain consistency
  5. 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

  1. Single source of truth: Database is authoritative for billing state
  2. Better performance: Cached data reduces Stripe API calls
  3. Improved security: No client-side Stripe operations
  4. Flexibility: Easy to add custom billing logic or switch providers
  5. Consistency: All billing operations follow same pattern
  6. Auditability: Complete trail of billing changes

Negative

  1. Complexity: More moving parts than direct Stripe integration
  2. Sync lag: Database may be slightly out of sync with Stripe
  3. Maintenance: Need to maintain sync logic and handle edge cases

Neutral

  1. Testing: Requires mocking GraphQL layer for tests
  2. Monitoring: Need to monitor both database and Stripe state
  3. 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

  • ADR-0003: GraphQL Schema-First TypeScript
  • ADR-0006: Fail-Closed Authentication
  • ADR-0011: Comprehensive Design System Enhancements