# 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