# Stripe Integration Architecture ## 🏗️ Simplified Webhook-Based Architecture ### Overview This integration uses a **pure webhook-based approach** without Foreign Data Wrappers: ``` ┌─────────────┐ │ Stripe │ (Source of truth) └──────┬──────┘ │ Webhooks ↓ ┌─────────────┐ │ Node.js │ (Webhook processor) │ API │ └──────┬──────┘ │ SQL Functions ↓ ┌─────────────┐ │ Supabase │ (Data storage) │ Database │ └──────┬──────┘ │ Direct queries (RLS) ↓ ┌─────────────┐ │ Frontend │ (React app) │ (Vite) │ └─────────────┘ ``` ## 📊 Data Flow Patterns ### Read Pattern (Fast ⚡) ``` Frontend → Supabase Client → RLS Policies → Database Tables ``` **Examples:** - Check if user is paying: `user.is_paying` - Get subscription: `useSubscription()` → queries `stripe_subscriptions` - Get prices: `useStripePrices()` → queries `stripe_prices` **Benefits:** - No API latency - Real-time capable - Secure via RLS - Automatic caching ### Write Pattern (via Stripe) ``` Frontend → API → Stripe → Webhook → Database ↓ (Direct update for instant feedback) ``` **Examples:** - Create subscription → API creates checkout → Stripe processes → Webhook syncs - Cancel subscription → API updates Stripe → immediately updates DB → Webhook confirms - Manage subscription → Opens Stripe portal → Stripe handles → Webhook syncs ## 🗄️ Database Schema ### Tables **stripe_customers** ```sql - id (PK) - user_id (FK → auth.users) - stripe_customer_id (unique) - email ``` **stripe_subscriptions** ```sql - id (PK) - user_id (FK → auth.users) - stripe_customer_id (FK) - status (active, trialing, canceled, etc.) - price_id - current_period_start - current_period_end - cancel_at_period_end ``` **stripe_products** ```sql - id (PK) - name ("Standard") - active - description ``` **stripe_prices** ```sql - id (PK) - product_id (FK) - unit_amount (in cents) - currency - interval (month, year) ``` **profiles** (extended) ```sql - is_paying (boolean) ← Auto-updated by trigger - subscription_tier (text) ← Auto-updated by trigger ``` ### RLS Policies ```sql -- Users see only their own data stripe_customers: WHERE auth.uid() = user_id stripe_subscriptions: WHERE auth.uid() = user_id -- Products/prices are public (for pricing page) stripe_products: WHERE true stripe_prices: WHERE true ``` ### Automatic Triggers ```sql -- When subscription changes → Update profile CREATE TRIGGER update_profile_on_subscription_change AFTER INSERT OR UPDATE ON stripe_subscriptions → Updates profiles.is_paying and profiles.subscription_tier ``` ## 🔄 Webhook Flow ### 1. Stripe Event Occurs ``` User subscribes → Stripe creates subscription ``` ### 2. Webhook Sent ``` POST https://your-api.com/api/v1/stripe/webhook Headers: stripe-signature Body: { type: "customer.subscription.created", data: { object: { ...subscription } } } ``` ### 3. API Processes Event ```typescript // stripe-webhook.ts 1. Verify signature 2. Parse event type 3. Call appropriate database function ``` ### 4. Database Function Executes ```sql -- handle_stripe_subscription_upsert() 1. Upsert subscription record 2. Trigger fires automatically 3. Profile updated (is_paying = true) ``` ### 5. Frontend Sees Update ```typescript // Next time query runs const { data: subscription } = useSubscription(); // Returns updated data with RLS filtering ``` ## 🔐 Security Model ### Frontend → Database ``` Supabase Client (anon key) → RLS Policies → Only own user_id data → Read-only access ``` **Frontend CANNOT:** - ❌ Modify subscription data - ❌ See other users' subscriptions - ❌ Delete customer records **Frontend CAN:** - ✅ Read own subscription - ✅ Read public products/prices - ✅ Call API endpoints for actions ### API → Database ``` Supabase Client (service role key) → Full database access → Used only in webhook handlers → Validates Stripe signature first ``` **API CAN:** - ✅ Insert/update via webhook functions - ✅ Query any subscription for operations - ✅ Execute privileged database functions ### Webhook → Database ``` Stripe Webhook → Signature verified → Service role functions → Bypass RLS for writes ``` ## 🎯 Why This Architecture? ### No Foreign Data Wrappers **Reasons:** - ❌ Extra complexity - ❌ Requires Vault configuration - ❌ API rate limits apply - ❌ Slower than cached data - ✅ **Webhooks provide all needed data** ### Direct Supabase Access **Benefits:** - ⚡ **Performance**: No API hop for reads - 🔒 **Security**: RLS enforces data access - 🎯 **Simplicity**: Standard Supabase patterns - 📊 **Real-time**: Can use Supabase subscriptions - 🔌 **Offline**: Can cache subscription data ### API for Actions Only **Why:** - 🔑 **Secret management**: API has Stripe secret key - ✅ **Validation**: Server-side validation before Stripe calls - 🎫 **Checkout**: Creates secure checkout sessions - 🔐 **Signatures**: Verifies webhook signatures ## 📈 Performance Characteristics ### Query Performance **Check if user is paying:** ```typescript user.is_paying // Instant (already in memory) ``` **Get subscription details:** ```typescript useSubscription() // ~50-100ms (Supabase query with join) // Cached for 5 minutes ``` **Start checkout:** ```typescript useCreateCheckoutSession() // ~500-1000ms (API → Stripe → Redirect) ``` ### Webhook Processing **Typical webhook flow:** ``` Stripe event → API receives → DB function → Trigger Total: ~100-300ms ``` **Database trigger:** ``` Subscription updated → Profile updated Total: ~10-50ms ``` ## 🎨 Frontend Usage Patterns ### Pattern 1: Simple Payment Check ```typescript const user = useUser(); if (!user.is_paying) { return ; } return ; ``` ### Pattern 2: Detailed Subscription Info ```typescript const { data: subscription, isLoading } = useSubscription(); if (isLoading) return ; if (!subscription) return ; return (
Status: {subscription.status} Renews: {subscription.current_period_end} {subscription.cancel_at_period_end && }
); ``` ### Pattern 3: Pricing Page ```typescript const { data: prices } = useStripePrices(); const { mutate: checkout } = useCreateCheckoutSession(); return prices?.map(price => ( checkout({ priceId: price.id })} /> )); ``` ## 🔧 Maintenance ### Syncing Products/Prices Products and prices are synced automatically via webhooks when you: 1. Create/update product in Stripe Dashboard 2. Webhook fires → Syncs to `stripe_products` table 3. Frontend queries updated pricing ### Monitoring Subscriptions ```sql -- View current user's active subscription (secure, RLS-compliant) SELECT * FROM get_my_active_subscription(); -- Check for subscription status distribution (requires direct stripe schema access) SELECT status::text, COUNT(*) as count FROM stripe.subscriptions GROUP BY status; -- Find subscriptions ending soon (admin query) SELECT p.email, si.current_period_end FROM stripe.subscriptions s JOIN stripe.customers c ON c.id = s.customer JOIN stripe.subscription_items si ON si.subscription = s.id JOIN profiles p ON p.id = (c.metadata->>'user_id')::uuid WHERE to_timestamp(si.current_period_end) < NOW() + INTERVAL '7 days' AND s.status::text = 'active'; ``` ## 📝 Summary **This architecture provides:** 1. ✅ **Simple**: No wrappers, just webhooks + RLS 2. ✅ **Fast**: Direct Supabase queries for reads 3. ✅ **Secure**: RLS + signature verification 4. ✅ **Maintainable**: Standard patterns 5. ✅ **Scalable**: Handles thousands of subscriptions 6. ✅ **Cost-effective**: Fewer API calls 7. ✅ **Developer-friendly**: TypeScript + React hooks **Trade-offs:** - ⏱️ Eventual consistency (webhook delay ~1-5 seconds) - 🔄 Requires webhook endpoint to be reliable - 📦 More database tables vs API-only approach **Verdict:** ✅ Optimal for SaaS apps with subscription model --- **Implementation Files:** - Database: `sql/35_stripe_wrappers.sql`, `sql/36_stripe_webhooks.sql` - Backend: `api/src/stripe.ts`, `api/src/stripe-webhook.ts` - Frontend: `apps/main/src/hooks/stripe.ts`, `apps/main/src/components/SubscriptionCard.tsx` - Types: `packages/shared/src/types/stripe.types.ts`