279 lines
6.9 KiB
Markdown
279 lines
6.9 KiB
Markdown
|
|
# Stripe Integration Using @supabase/stripe-sync-engine
|
||
|
|
|
||
|
|
## 📦 Overview
|
||
|
|
|
||
|
|
We're using the official [@supabase/stripe-sync-engine](https://github.com/supabase/stripe-sync-engine) library to handle all Stripe webhook processing and data syncing.
|
||
|
|
|
||
|
|
**Benefits:**
|
||
|
|
- ✅ Battle-tested by Supabase
|
||
|
|
- ✅ Handles all Stripe webhook events automatically
|
||
|
|
- ✅ Automatic schema management
|
||
|
|
- ✅ Real-time sync of Stripe data
|
||
|
|
- ✅ No custom webhook handlers needed
|
||
|
|
|
||
|
|
## 🚀 Quick Setup
|
||
|
|
|
||
|
|
### 1. Install Package
|
||
|
|
```bash
|
||
|
|
cd api
|
||
|
|
npm install @supabase/stripe-sync-engine
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Configure Environment
|
||
|
|
|
||
|
|
Add to `api/.env`:
|
||
|
|
```env
|
||
|
|
# Stripe
|
||
|
|
STRIPE_SECRET_KEY=sk_test_xxxxx
|
||
|
|
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
|
||
|
|
|
||
|
|
# Database (direct Postgres connection)
|
||
|
|
DATABASE_URL=postgresql://postgres:[password]@db.[project].supabase.co:5432/postgres
|
||
|
|
|
||
|
|
# Or use Supabase connection string
|
||
|
|
SUPABASE_URL=https://[project].supabase.co
|
||
|
|
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Run Library Migrations
|
||
|
|
|
||
|
|
The stripe-sync-engine library comes with its own migrations. Run them:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// In a one-time setup script or manually
|
||
|
|
import { runMigrations } from '@supabase/stripe-sync-engine';
|
||
|
|
|
||
|
|
await runMigrations({
|
||
|
|
databaseUrl: process.env.DATABASE_URL
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
Or manually execute the migrations from: `node_modules/@supabase/stripe-sync-engine/dist/migrations/*.sql`
|
||
|
|
|
||
|
|
### 4. Integration is Already Done! ✅
|
||
|
|
|
||
|
|
The webhook handler in `api/src/stripe.ts` is already configured:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { StripeSync } from "@supabase/stripe-sync-engine";
|
||
|
|
|
||
|
|
const stripeSync = new StripeSync({
|
||
|
|
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
|
||
|
|
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
||
|
|
schema: "public",
|
||
|
|
poolConfig: {
|
||
|
|
connectionString: process.env.DATABASE_URL,
|
||
|
|
max: 10,
|
||
|
|
},
|
||
|
|
revalidateObjectsViaStripeApi: ['subscription', 'customer'],
|
||
|
|
});
|
||
|
|
|
||
|
|
// Webhook endpoint
|
||
|
|
stripeRouter.post("/webhook", async (c) => {
|
||
|
|
const signature = c.req.header("stripe-signature");
|
||
|
|
const rawBody = await c.req.text();
|
||
|
|
|
||
|
|
await stripeSync.processWebhook(rawBody, signature);
|
||
|
|
|
||
|
|
return c.json({ received: true });
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## 📊 Database Schema
|
||
|
|
|
||
|
|
The library creates these tables in the `public` schema (with `stripe_` prefix):
|
||
|
|
|
||
|
|
### Core Tables (Auto-created)
|
||
|
|
- `stripe_customers`
|
||
|
|
- `stripe_subscriptions`
|
||
|
|
- `stripe_subscription_items`
|
||
|
|
- `stripe_products`
|
||
|
|
- `stripe_prices`
|
||
|
|
- `stripe_invoices`
|
||
|
|
- `stripe_charges`
|
||
|
|
- `stripe_payment_intents`
|
||
|
|
- `stripe_payment_methods`
|
||
|
|
- And many more...
|
||
|
|
|
||
|
|
### Custom Additions (From our SQL)
|
||
|
|
We still need our custom `sql/35_stripe_wrappers.sql` for:
|
||
|
|
- ✅ `profiles.is_paying` column
|
||
|
|
- ✅ `profiles.subscription_tier` column
|
||
|
|
- ✅ Triggers to auto-update profile when subscription changes
|
||
|
|
- ✅ RLS policies
|
||
|
|
- ✅ Helper functions (`is_paying_user()`, etc.)
|
||
|
|
|
||
|
|
**Note:** Delete `sql/36_stripe_webhooks.sql` - not needed, library handles webhooks!
|
||
|
|
|
||
|
|
## 🔄 How It Works
|
||
|
|
|
||
|
|
### 1. Webhook Received
|
||
|
|
```
|
||
|
|
Stripe → POST /api/v1/stripe/webhook → StripeSync.processWebhook()
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Sync Engine Processes
|
||
|
|
```
|
||
|
|
1. Verifies signature
|
||
|
|
2. Determines event type
|
||
|
|
3. Syncs to appropriate table (stripe_subscriptions, stripe_customers, etc.)
|
||
|
|
4. Handles all edge cases automatically
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Our Custom Trigger Fires
|
||
|
|
```
|
||
|
|
stripe_subscriptions updated → Trigger → profiles.is_paying updated
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. Frontend Queries
|
||
|
|
```
|
||
|
|
Frontend → Supabase Client → stripe_subscriptions (RLS) → User's data
|
||
|
|
```
|
||
|
|
|
||
|
|
## ✨ What the Library Handles
|
||
|
|
|
||
|
|
The stripe-sync-engine automatically handles:
|
||
|
|
|
||
|
|
- ✅ All webhook event types (100+ events)
|
||
|
|
- ✅ Signature verification
|
||
|
|
- ✅ Idempotency (duplicate events)
|
||
|
|
- ✅ Foreign key relationships
|
||
|
|
- ✅ Deleted entities
|
||
|
|
- ✅ List expansion (fetching all items)
|
||
|
|
- ✅ Backfilling historical data
|
||
|
|
- ✅ Database schema migrations
|
||
|
|
- ✅ Connection pooling
|
||
|
|
- ✅ Error handling
|
||
|
|
|
||
|
|
## 🎯 What We Still Handle
|
||
|
|
|
||
|
|
We only need to maintain:
|
||
|
|
|
||
|
|
1. **Custom Profile Fields**
|
||
|
|
- `profiles.is_paying`
|
||
|
|
- `profiles.subscription_tier`
|
||
|
|
|
||
|
|
2. **Automatic Update Trigger**
|
||
|
|
```sql
|
||
|
|
CREATE TRIGGER update_profile_on_subscription_change
|
||
|
|
AFTER INSERT OR UPDATE ON stripe_subscriptions
|
||
|
|
→ Updates profile fields
|
||
|
|
```
|
||
|
|
|
||
|
|
3. **Action Endpoints** (in `api/src/stripe.ts`)
|
||
|
|
- Create checkout session
|
||
|
|
- Create portal session
|
||
|
|
- Cancel subscription
|
||
|
|
- Reactivate subscription
|
||
|
|
|
||
|
|
4. **RLS Policies**
|
||
|
|
- Ensure users only see their own data
|
||
|
|
|
||
|
|
## 🧪 Testing
|
||
|
|
|
||
|
|
### Test Webhook Processing
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Terminal 1: Start your API
|
||
|
|
cd api && npm run dev
|
||
|
|
|
||
|
|
# Terminal 2: Forward Stripe webhooks
|
||
|
|
stripe listen --forward-to http://localhost:3000/api/v1/stripe/webhook
|
||
|
|
|
||
|
|
# Terminal 3: Trigger test event
|
||
|
|
stripe trigger customer.subscription.created
|
||
|
|
```
|
||
|
|
|
||
|
|
**Watch the magic:**
|
||
|
|
1. Webhook arrives
|
||
|
|
2. stripe-sync-engine processes it
|
||
|
|
3. Data appears in `stripe_subscriptions` table
|
||
|
|
4. Trigger updates `profiles.is_paying`
|
||
|
|
5. Frontend sees update immediately
|
||
|
|
|
||
|
|
### Verify Sync
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- Check synced subscription
|
||
|
|
SELECT * FROM stripe_subscriptions;
|
||
|
|
|
||
|
|
-- Check synced customer
|
||
|
|
SELECT * FROM stripe_customers;
|
||
|
|
|
||
|
|
-- Check profile updated
|
||
|
|
SELECT email, is_paying, subscription_tier FROM profiles;
|
||
|
|
```
|
||
|
|
|
||
|
|
## 📝 Environment Variables Needed
|
||
|
|
|
||
|
|
```env
|
||
|
|
# Required
|
||
|
|
STRIPE_SECRET_KEY=sk_test_xxxxx
|
||
|
|
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
|
||
|
|
DATABASE_URL=postgresql://postgres:password@host:port/db
|
||
|
|
|
||
|
|
# Optional (if DATABASE_URL not set)
|
||
|
|
SUPABASE_URL=https://xxx.supabase.co
|
||
|
|
SUPABASE_SERVICE_ROLE_KEY=eyJxxx...
|
||
|
|
```
|
||
|
|
|
||
|
|
## 🔧 Backfilling Historical Data
|
||
|
|
|
||
|
|
If you have existing Stripe data, backfill it:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Run this once to sync all existing data
|
||
|
|
await stripeSync.syncBackfill({
|
||
|
|
object: 'all', // or specific: 'customer', 'subscription', 'product', etc.
|
||
|
|
created: {
|
||
|
|
gte: 0 // Sync everything from the beginning
|
||
|
|
}
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
Or sync specific objects:
|
||
|
|
```typescript
|
||
|
|
await stripeSync.syncProducts();
|
||
|
|
await stripeSync.syncPrices();
|
||
|
|
await stripeSync.syncCustomers();
|
||
|
|
await stripeSync.syncSubscriptions();
|
||
|
|
```
|
||
|
|
|
||
|
|
## 📚 Resources
|
||
|
|
|
||
|
|
- **Library Repo**: https://github.com/supabase/stripe-sync-engine
|
||
|
|
- **Library Docs**: https://supabase.github.io/stripe-sync-engine
|
||
|
|
- **Our Implementation**: `api/src/stripe.ts`
|
||
|
|
- **Custom SQL**: `sql/35_stripe_wrappers.sql` (profile fields & triggers only)
|
||
|
|
|
||
|
|
## ⚡ Benefits Over Custom Implementation
|
||
|
|
|
||
|
|
| Feature | Custom | stripe-sync-engine |
|
||
|
|
|---------|--------|-------------------|
|
||
|
|
| Webhook handlers | Manual | ✅ Automatic |
|
||
|
|
| Schema management | Manual | ✅ Automatic |
|
||
|
|
| Edge cases | You handle | ✅ Handled |
|
||
|
|
| Updates | You maintain | ✅ Library updates |
|
||
|
|
| Testing | Your responsibility | ✅ Battle-tested |
|
||
|
|
| Backfilling | Custom scripts | ✅ Built-in |
|
||
|
|
|
||
|
|
## 🎉 Summary
|
||
|
|
|
||
|
|
**Before:** 267 lines of custom webhook code + SQL handlers
|
||
|
|
**After:** ~20 lines using the library
|
||
|
|
|
||
|
|
**What you maintain:**
|
||
|
|
- Profile integration (`is_paying`, `subscription_tier`)
|
||
|
|
- Action endpoints (checkout, portal, cancel)
|
||
|
|
- RLS policies
|
||
|
|
- Frontend hooks
|
||
|
|
|
||
|
|
**What the library handles:**
|
||
|
|
- Everything else! 🚀
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Status**: ✅ Integrated
|
||
|
|
**Next**: Configure Stripe, test webhooks, add to settings page
|
||
|
|
|