Source:
ocean/docs/adr/0030-database-views-for-billing.md| ✏️ Edit on GitHub
0030. Database Views for Complex Billing Queries
Date: 2025-08-14
Status
Accepted
Context
The billing overview page was executing multiple complex queries with joins across 5+ tables:
- organizations
- subscription_items
- stripe_prices
- stripe_products
- payment_methods
- invoices
- usage_records
This resulted in:
- Slow page loads (2-3 seconds)
- Complex resolver logic
- Difficult to maintain SQL queries
- High database CPU usage
Decision
We created optimized database views that pre-join and aggregate billing data, focusing on Supabase as the source of truth for application state while leaving analytics to Stripe.
Views Created
- billing_dashboard: Comprehensive billing information with pre-joined subscription and invoice data
- organization_billing_summary: Simplified view for listing organizations
- subscription_details: Detailed subscription information with expanded price/product data
Key Design Principles
- Supabase stores application state (what plan, what resources)
- Stripe handles revenue analytics (MRR, churn, etc.)
- Views optimize for read performance
- No duplication of Stripe's analytics capabilities
Consequences
Positive
- Performance: Billing page load reduced from 2s to 200ms
- Maintainability: Complex joins centralized in database
- Consistency: Single source of truth for billing state
- Flexibility: Easy to add new fields to views
Negative
- Schema Coupling: Views depend on table structure
- Migration Complexity: View updates require careful planning
- Storage: Slight increase in database storage
Performance Improvements
-- Before: 6 separate queries with complex joins
-- After: 1 query
SELECT * FROM billing_dashboard WHERE organization_id = ?
Alternatives Considered
-
Materialized Views Everywhere
- Rejected: Unnecessary complexity for data that changes frequently
-
GraphQL Schema Stitching
- Rejected: Would require direct Stripe API calls from frontend
-
Caching Layer Only
- Rejected: Still requires complex queries, just less frequently
Migration Strategy
- Create views without disrupting existing queries
- Update GraphQL resolvers to use views
- Monitor performance
- Remove old query logic