Add docs for stripe
This commit is contained in:
parent
40ce04ecc0
commit
f6acbef13d
4 changed files with 1152 additions and 0 deletions
349
docs/STRIPE_IMPLEMENTATION_SUMMARY.md
Normal file
349
docs/STRIPE_IMPLEMENTATION_SUMMARY.md
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
# Stripe Integration Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
Complete Stripe integration for Xtablo with a single "Standard" subscription plan.
|
||||
Uses custom Node.js API (Hono) with Supabase database for data storage.
|
||||
|
||||
## ✅ What Has Been Implemented
|
||||
|
||||
### 1. Database Schema (`sql/35_stripe_wrappers.sql`)
|
||||
|
||||
#### Foreign Tables (Direct Stripe API Access)
|
||||
- `stripe.customers` - Query Stripe customers
|
||||
- `stripe.subscriptions` - Query Stripe subscriptions
|
||||
- `stripe.products` - Query Stripe products
|
||||
- `stripe.prices` - Query Stripe prices
|
||||
|
||||
#### Local Tables (Synced via Webhooks)
|
||||
- `public.stripe_customers` - Customer records with user mapping
|
||||
- `public.stripe_subscriptions` - Subscription history
|
||||
- `public.stripe_products` - Product catalog
|
||||
- `public.stripe_prices` - Pricing information
|
||||
|
||||
#### Profile Enhancements
|
||||
- `profiles.is_paying` - Boolean flag for quick checks
|
||||
- `profiles.subscription_tier` - Current tier ('free' or 'standard')
|
||||
|
||||
#### Helper Functions
|
||||
```sql
|
||||
-- Check if user is paying
|
||||
SELECT is_paying_user(auth.uid());
|
||||
|
||||
-- Get subscription details
|
||||
SELECT * FROM get_user_subscription_status(auth.uid());
|
||||
|
||||
-- Get Stripe customer ID
|
||||
SELECT get_user_stripe_customer_id(auth.uid());
|
||||
```
|
||||
|
||||
#### Views
|
||||
- `active_subscriptions` - All active subs with user info
|
||||
|
||||
#### Automatic Updates
|
||||
- Triggers automatically update `profiles.is_paying` when subscriptions change
|
||||
- `updated_at` timestamps managed automatically
|
||||
|
||||
### 2. Webhook Handlers (`sql/36_stripe_webhooks.sql`)
|
||||
|
||||
Includes functions to process all major Stripe events:
|
||||
|
||||
**Customer Events:**
|
||||
- `handle_stripe_customer_created()`
|
||||
- `handle_stripe_customer_updated()`
|
||||
- `handle_stripe_customer_deleted()`
|
||||
|
||||
**Product Events:**
|
||||
- `handle_stripe_product_upsert()`
|
||||
- `handle_stripe_product_deleted()`
|
||||
|
||||
**Price Events:**
|
||||
- `handle_stripe_price_upsert()`
|
||||
- `handle_stripe_price_deleted()`
|
||||
|
||||
**Subscription Events:**
|
||||
- `handle_stripe_subscription_upsert()`
|
||||
- `handle_stripe_subscription_deleted()`
|
||||
|
||||
### 3. Backend API (`api/src/stripe.ts` & `api/src/stripe-webhook.ts`)
|
||||
|
||||
**Endpoints:**
|
||||
- `POST /api/v1/stripe/webhook` - Stripe webhook handler
|
||||
- `POST /api/v1/stripe/create-checkout-session` - Start subscription checkout
|
||||
- `POST /api/v1/stripe/create-portal-session` - Open customer portal
|
||||
- `GET /api/v1/stripe/subscription` - Get user's subscription
|
||||
- `GET /api/v1/stripe/is-paying` - Check payment status
|
||||
- `GET /api/v1/stripe/prices` - Get Standard plan prices
|
||||
- `POST /api/v1/stripe/cancel-subscription` - Cancel at period end
|
||||
- `POST /api/v1/stripe/reactivate-subscription` - Reactivate subscription
|
||||
|
||||
**Features:**
|
||||
- ✅ Automatic customer creation
|
||||
- ✅ Secure webhook signature verification
|
||||
- ✅ Complete subscription lifecycle management
|
||||
- ✅ Customer portal access
|
||||
- ✅ Subscription cancellation/reactivation
|
||||
|
||||
### 4. Frontend Hooks (`apps/main/src/hooks/stripe.ts`)
|
||||
|
||||
**Available Hooks:**
|
||||
- `useSubscription()` - Get subscription details
|
||||
- `useIsPayingUser()` - Check if user is paying
|
||||
- `useStripePrices()` - Get available prices
|
||||
- `useCreateCheckoutSession()` - Start checkout
|
||||
- `useCreatePortalSession()` - Open portal
|
||||
- `useCancelSubscription()` - Cancel subscription
|
||||
- `useReactivateSubscription()` - Reactivate subscription
|
||||
|
||||
### 5. TypeScript Types (`packages/shared/src/types/stripe.types.ts`)
|
||||
|
||||
Complete type definitions:
|
||||
- `StripeCustomer`
|
||||
- `StripeSubscription`
|
||||
- `StripeProduct`
|
||||
- `StripePrice`
|
||||
- Helper functions for formatting and status checks
|
||||
|
||||
### 6. Documentation (`docs/STRIPE_SETUP.md`)
|
||||
|
||||
Comprehensive setup guide including:
|
||||
- Step-by-step configuration
|
||||
- Edge Function example
|
||||
- Frontend integration examples
|
||||
- Testing instructions
|
||||
- Troubleshooting tips
|
||||
|
||||
## 🎯 Key Features
|
||||
|
||||
### Security
|
||||
- ✅ Row Level Security (RLS) on all tables
|
||||
- ✅ Users can only see their own data
|
||||
- ✅ Stripe API key stored securely in Vault
|
||||
- ✅ Webhook signature verification
|
||||
- ✅ Service role functions for webhooks
|
||||
|
||||
### Performance
|
||||
- ✅ Indexed on user_id, customer_id, status
|
||||
- ✅ Efficient queries for subscription checks
|
||||
- ✅ Cached subscription status in profiles table
|
||||
|
||||
### Data Integrity
|
||||
- ✅ Foreign key relationships
|
||||
- ✅ Cascading deletes
|
||||
- ✅ Automatic timestamp management
|
||||
- ✅ Transaction safety in webhook handlers
|
||||
|
||||
## 📊 How to Check if User is Paying
|
||||
|
||||
### Method 1: From Profile (Fastest)
|
||||
```typescript
|
||||
const user = useUser();
|
||||
if (user.is_paying) {
|
||||
// User has active Standard subscription
|
||||
console.log(`User is on ${user.subscription_tier} plan`); // 'standard'
|
||||
}
|
||||
```
|
||||
|
||||
### Method 2: Using Function (Most Accurate)
|
||||
```typescript
|
||||
const { data: isPaying } = useQuery({
|
||||
queryKey: ['is_paying', userId],
|
||||
queryFn: async () => {
|
||||
const { data } = await supabase.rpc('is_paying_user', {
|
||||
user_uuid: userId
|
||||
});
|
||||
return data;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Method 3: Direct Query
|
||||
```typescript
|
||||
const { data: subscription } = await supabase
|
||||
.from('stripe_subscriptions')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.in('status', ['active', 'trialing'])
|
||||
.single();
|
||||
|
||||
const isPaying = !!subscription;
|
||||
```
|
||||
|
||||
## 🔄 Data Flow
|
||||
|
||||
1. **User Clicks "Subscribe to Standard"**
|
||||
→ Frontend calls `/api/v1/stripe/create-checkout-session`
|
||||
|
||||
2. **API Creates Checkout**
|
||||
→ Creates/retrieves Stripe customer
|
||||
→ Creates checkout session
|
||||
→ Returns checkout URL
|
||||
|
||||
3. **User Completes Payment**
|
||||
→ Stripe processes payment
|
||||
→ Fires `customer.subscription.created` webhook
|
||||
|
||||
4. **Webhook Received**
|
||||
→ API endpoint `/api/v1/stripe/webhook` receives event
|
||||
→ Verifies signature
|
||||
→ Calls `handle_stripe_subscription_upsert()`
|
||||
|
||||
5. **Database Updated**
|
||||
→ Inserts into `stripe_subscriptions`
|
||||
→ Trigger updates `profiles.is_paying = true` and `subscription_tier = 'standard'`
|
||||
|
||||
6. **Frontend Updates**
|
||||
→ User's profile automatically shows paying status
|
||||
→ `user.is_paying = true`
|
||||
→ `user.subscription_tier = 'standard'`
|
||||
|
||||
## 🚀 Next Steps to Complete Implementation
|
||||
|
||||
### Required Setup:
|
||||
|
||||
1. **Update Vault Key ID**
|
||||
- Get key_id after storing Stripe API key
|
||||
- Update in `sql/35_stripe_wrappers.sql` line 27
|
||||
|
||||
2. **Install Stripe SDK**
|
||||
```bash
|
||||
cd api
|
||||
npm install stripe @stripe/stripe-js
|
||||
```
|
||||
|
||||
3. **Configure API Environment Variables**
|
||||
Add to `api/.env`:
|
||||
```env
|
||||
STRIPE_SECRET_KEY=sk_test_xxxxx
|
||||
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
|
||||
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
|
||||
```
|
||||
|
||||
4. **Configure Frontend Environment**
|
||||
Add to `apps/main/.env`:
|
||||
```env
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx
|
||||
```
|
||||
|
||||
5. **Create "Standard" Plan in Stripe**
|
||||
- Create product named exactly **"Standard"**
|
||||
- Add pricing (monthly/yearly)
|
||||
- Note price IDs for frontend
|
||||
|
||||
6. **Configure Webhook in Stripe Dashboard**
|
||||
- Add endpoint: `https://your-api.com/api/v1/stripe/webhook`
|
||||
- Select all customer/subscription/product/price events
|
||||
- Copy webhook signing secret
|
||||
|
||||
### Frontend Implementation:
|
||||
|
||||
7. **Hooks Already Created** ✅
|
||||
- `useSubscription()` - Get subscription details
|
||||
- `useIsPayingUser()` - Check payment status
|
||||
- `useCreateCheckoutSession()` - Initiate checkout
|
||||
- `useCreatePortalSession()` - Customer portal
|
||||
- `useCancelSubscription()` - Cancel subscription
|
||||
- `useReactivateSubscription()` - Reactivate
|
||||
|
||||
8. **Build UI Components**
|
||||
- Pricing page
|
||||
- Subscription management page
|
||||
- Payment status badges
|
||||
- Upgrade prompts
|
||||
|
||||
7. **Add Feature Gates**
|
||||
```typescript
|
||||
if (!user.is_paying) {
|
||||
return <PremiumFeatureLockedPrompt />;
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 Environment Variables Needed
|
||||
|
||||
### API (`api/.env`)
|
||||
```env
|
||||
# Stripe
|
||||
STRIPE_SECRET_KEY=sk_test_xxxxx (or sk_live_xxxxx)
|
||||
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
|
||||
|
||||
# Supabase
|
||||
SUPABASE_URL=https://xxx.supabase.co
|
||||
SUPABASE_SERVICE_ROLE_KEY=eyJxxx...
|
||||
|
||||
# Frontend URL (for redirects)
|
||||
FRONTEND_URL=http://localhost:5173 (or https://app.xtablo.com)
|
||||
```
|
||||
|
||||
### Frontend (`apps/main/.env`)
|
||||
```env
|
||||
# Stripe
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx (or pk_live_xxxxx)
|
||||
```
|
||||
|
||||
## 🧪 Testing Checklist
|
||||
|
||||
- [ ] Run database migrations (35 & 36)
|
||||
- [ ] Install Stripe SDK in API: `npm install stripe @stripe/stripe-js`
|
||||
- [ ] Install Stripe SDK in frontend: `npm install @stripe/stripe-js`
|
||||
- [ ] Configure environment variables (API + Frontend)
|
||||
- [ ] Create "Standard" product in Stripe Dashboard
|
||||
- [ ] Configure webhook endpoint in Stripe
|
||||
- [ ] Test webhook with Stripe CLI
|
||||
- [ ] Can create checkout session from frontend
|
||||
- [ ] Payment redirects back to app
|
||||
- [ ] Webhook syncs subscription to database
|
||||
- [ ] `profiles.is_paying` updates automatically
|
||||
- [ ] Users can only see their own subscription data
|
||||
- [ ] `is_paying_user()` function returns correct status
|
||||
- [ ] Can access customer portal
|
||||
- [ ] Subscription cancellation updates status
|
||||
- [ ] Test cards work in test mode
|
||||
|
||||
## 📚 Reference
|
||||
|
||||
- **Database Schema**: `sql/35_stripe_wrappers.sql`
|
||||
- **Webhook Handlers**: `sql/36_stripe_webhooks.sql`
|
||||
- **API Routes**: `api/src/stripe.ts`
|
||||
- **Webhook Handler**: `api/src/stripe-webhook.ts`
|
||||
- **Frontend Hooks**: `apps/main/src/hooks/stripe.ts`
|
||||
- **TypeScript Types**: `packages/shared/src/types/stripe.types.ts`
|
||||
- **Setup Guide**: `docs/STRIPE_SETUP.md`
|
||||
- **Stripe API Docs**: https://stripe.com/docs/api
|
||||
- **Supabase Wrappers**: https://supabase.github.io/wrappers/stripe/
|
||||
|
||||
## 🎉 Benefits
|
||||
|
||||
1. **Direct Stripe Access** - Query Stripe data without API calls
|
||||
2. **Instant Status Checks** - `is_paying` flag on user profile
|
||||
3. **Automatic Sync** - Webhooks keep data current
|
||||
4. **Secure** - RLS ensures data privacy
|
||||
5. **Type-Safe** - Full TypeScript support
|
||||
6. **Scalable** - Handles thousands of subscriptions
|
||||
7. **Historical Data** - Keep subscription history
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Single "Standard" Plan Architecture
|
||||
|
||||
This implementation is optimized for a single subscription tier:
|
||||
|
||||
**Plan Structure:**
|
||||
- **Free** - Default tier, `is_paying = false`
|
||||
- **Standard** - Paid tier, `is_paying = true`, `subscription_tier = 'standard'`
|
||||
|
||||
**Why This Works:**
|
||||
- Simple pricing model
|
||||
- Easy to check: just `user.is_paying`
|
||||
- Future-proof: can add more tiers later by updating the tier logic
|
||||
- Webhook automatically handles upgrades/downgrades
|
||||
|
||||
**API Endpoints Specific to Standard:**
|
||||
- `GET /api/v1/stripe/prices` - Returns only "Standard" plan prices
|
||||
- Creates customers with metadata linking to user_id
|
||||
- All subscriptions automatically set tier to 'standard'
|
||||
|
||||
---
|
||||
|
||||
**Implementation Status**: ✅ Complete (Database, API, Frontend, Types)
|
||||
**Next Step**: Configure Stripe and test webhook integration
|
||||
|
||||
190
docs/STRIPE_QUICK_REFERENCE.md
Normal file
190
docs/STRIPE_QUICK_REFERENCE.md
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
# Stripe Integration - Quick Reference
|
||||
|
||||
## 🚀 Quick Start (For Your Node.js API)
|
||||
|
||||
### 1. Install Dependencies
|
||||
```bash
|
||||
cd api && npm install stripe @stripe/stripe-js
|
||||
cd apps/main && npm install @stripe/stripe-js
|
||||
```
|
||||
|
||||
### 2. Environment Variables
|
||||
|
||||
**API (`.env`):**
|
||||
```env
|
||||
STRIPE_SECRET_KEY=sk_test_xxxxx
|
||||
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
|
||||
SUPABASE_SERVICE_ROLE_KEY=your_service_key
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
**Frontend (`apps/main/.env`):**
|
||||
```env
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx
|
||||
```
|
||||
|
||||
### 3. Stripe Dashboard Setup
|
||||
|
||||
1. Create product named **"Standard"**
|
||||
2. Add pricing (save price IDs)
|
||||
3. Add webhook: `https://your-api.com/api/v1/stripe/webhook`
|
||||
4. Copy webhook secret
|
||||
|
||||
### 4. Run Migrations
|
||||
```sql
|
||||
-- Execute in Supabase SQL Editor
|
||||
\i sql/35_stripe_wrappers.sql
|
||||
\i sql/36_stripe_webhooks.sql
|
||||
```
|
||||
|
||||
## 📋 API Endpoints
|
||||
|
||||
| Method | Endpoint | Auth | Description |
|
||||
|--------|----------|------|-------------|
|
||||
| POST | `/api/v1/stripe/webhook` | ❌ | Stripe webhook (signature verified) |
|
||||
| POST | `/api/v1/stripe/create-checkout-session` | ✅ | Start subscription flow |
|
||||
| POST | `/api/v1/stripe/create-portal-session` | ✅ | Open customer portal |
|
||||
| GET | `/api/v1/stripe/subscription` | ✅ | Get subscription details |
|
||||
| GET | `/api/v1/stripe/is-paying` | ✅ | Check if user is paying |
|
||||
| GET | `/api/v1/stripe/prices` | ❌ | Get Standard plan prices |
|
||||
| POST | `/api/v1/stripe/cancel-subscription` | ✅ | Cancel subscription |
|
||||
| POST | `/api/v1/stripe/reactivate-subscription` | ✅ | Reactivate subscription |
|
||||
|
||||
## 🎣 Frontend Hooks
|
||||
|
||||
```typescript
|
||||
import {
|
||||
useSubscription, // Get full subscription details
|
||||
useIsPayingUser, // Boolean: is user paying?
|
||||
useStripePrices, // Get available prices
|
||||
useCreateCheckoutSession, // Create checkout & redirect
|
||||
useCreatePortalSession, // Open customer portal
|
||||
useCancelSubscription, // Cancel at period end
|
||||
useReactivateSubscription // Undo cancellation
|
||||
} from '@/hooks/stripe';
|
||||
```
|
||||
|
||||
## 💡 Common Use Cases
|
||||
|
||||
### Check if User is Paying
|
||||
```typescript
|
||||
const user = useUser();
|
||||
if (user.is_paying) {
|
||||
// Show premium feature
|
||||
}
|
||||
```
|
||||
|
||||
### Subscribe to Standard Plan
|
||||
```typescript
|
||||
const { mutate: checkout } = useCreateCheckoutSession();
|
||||
|
||||
<button onClick={() => checkout({ priceId: 'price_xxxxx' })}>
|
||||
Subscribe to Standard
|
||||
</button>
|
||||
```
|
||||
|
||||
### Manage Subscription
|
||||
```typescript
|
||||
const { mutate: openPortal } = useCreatePortalSession();
|
||||
|
||||
<button onClick={() => openPortal()}>
|
||||
Manage Subscription
|
||||
</button>
|
||||
```
|
||||
|
||||
### Show Subscription Status
|
||||
```typescript
|
||||
const { data: subscription } = useSubscription();
|
||||
|
||||
{subscription?.status === 'active' && (
|
||||
<div>
|
||||
Active until {subscription.current_period_end}
|
||||
{subscription.cancel_at_period_end && (
|
||||
<span>Will cancel at period end</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
## 🔍 Database Queries
|
||||
|
||||
### Check Subscription in SQL
|
||||
```sql
|
||||
-- Is user paying?
|
||||
SELECT is_paying_user('user-uuid-here');
|
||||
|
||||
-- Get subscription details
|
||||
SELECT * FROM get_user_subscription_status('user-uuid-here');
|
||||
|
||||
-- Get all active subscriptions
|
||||
SELECT * FROM active_subscriptions;
|
||||
|
||||
-- Query Stripe directly
|
||||
SELECT * FROM stripe.subscriptions WHERE customer = 'cus_xxxxx';
|
||||
```
|
||||
|
||||
## 🎨 Profile Fields
|
||||
|
||||
After subscription sync, profiles have:
|
||||
```typescript
|
||||
{
|
||||
is_paying: boolean, // true if active/trialing subscription
|
||||
subscription_tier: string, // 'free' | 'standard'
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Test Cards
|
||||
|
||||
| Card | Result |
|
||||
|------|--------|
|
||||
| `4242 4242 4242 4242` | ✅ Success |
|
||||
| `4000 0000 0000 0002` | ❌ Declined |
|
||||
| `4000 0000 0000 9995` | ❌ Insufficient funds |
|
||||
|
||||
## 🔔 Webhook Events Handled
|
||||
|
||||
- ✅ `customer.created`
|
||||
- ✅ `customer.updated`
|
||||
- ✅ `customer.deleted`
|
||||
- ✅ `customer.subscription.created`
|
||||
- ✅ `customer.subscription.updated`
|
||||
- ✅ `customer.subscription.deleted`
|
||||
- ✅ `product.created` / `updated` / `deleted`
|
||||
- ✅ `price.created` / `updated` / `deleted`
|
||||
|
||||
## 🎯 Standard Plan Details
|
||||
|
||||
**Single Tier Model:**
|
||||
- **Free**: Default (no subscription)
|
||||
- **Standard**: Paid subscription
|
||||
|
||||
**Subscription automatically:**
|
||||
- Sets `is_paying = true`
|
||||
- Sets `subscription_tier = 'standard'`
|
||||
- Updates on webhook events
|
||||
- Reverts to free on cancellation
|
||||
|
||||
## 🔐 Security
|
||||
|
||||
- ✅ Webhook signature verification
|
||||
- ✅ Row Level Security on all tables
|
||||
- ✅ Users can only see their own subscriptions
|
||||
- ✅ Service role for webhook functions
|
||||
- ✅ Stripe keys in environment (never in code)
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For issues, check:
|
||||
1. API logs for webhook errors
|
||||
2. Stripe Dashboard → Webhooks → Event logs
|
||||
3. Supabase logs for database errors
|
||||
4. Browser console for frontend errors
|
||||
|
||||
---
|
||||
|
||||
**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`
|
||||
- Types: `packages/shared/src/types/stripe.types.ts`
|
||||
|
||||
254
docs/STRIPE_README.md
Normal file
254
docs/STRIPE_README.md
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
# 💳 Stripe Integration for Xtablo
|
||||
|
||||
Complete Stripe subscription integration with a single "Standard" plan using your Node.js API.
|
||||
|
||||
## 📁 Files Created
|
||||
|
||||
### Database (SQL)
|
||||
- `sql/35_stripe_wrappers.sql` - Database schema, tables, functions, triggers
|
||||
- `sql/36_stripe_webhooks.sql` - Webhook handler functions
|
||||
|
||||
### Backend (Node.js API)
|
||||
- `api/src/stripe.ts` - API routes for Stripe operations
|
||||
- `api/src/stripe-webhook.ts` - Webhook event processor
|
||||
- `api/src/routers.ts` - ✅ Updated with Stripe routes
|
||||
|
||||
### Frontend (React)
|
||||
- `apps/main/src/hooks/stripe.ts` - React hooks for Stripe
|
||||
- `apps/main/src/components/SubscriptionCard.tsx` - Ready-to-use subscription UI
|
||||
- `packages/shared/src/types/stripe.types.ts` - TypeScript types
|
||||
|
||||
### Documentation
|
||||
- `docs/STRIPE_SETUP.md` - Complete setup guide
|
||||
- `docs/STRIPE_IMPLEMENTATION_SUMMARY.md` - Technical overview
|
||||
- `docs/STRIPE_QUICK_REFERENCE.md` - Quick reference guide
|
||||
- `docs/STRIPE_README.md` - This file
|
||||
|
||||
## ⚡ Quick Setup
|
||||
|
||||
### 1. Install Dependencies
|
||||
```bash
|
||||
cd api && npm install stripe @stripe/stripe-js
|
||||
cd ../apps/main && npm install @stripe/stripe-js
|
||||
```
|
||||
|
||||
### 2. Run Database Migrations
|
||||
Execute in Supabase SQL Editor:
|
||||
```sql
|
||||
\i sql/35_stripe_wrappers.sql
|
||||
\i sql/36_stripe_webhooks.sql
|
||||
```
|
||||
|
||||
### 3. Configure Stripe Dashboard
|
||||
1. Create product named **"Standard"**
|
||||
2. Add monthly/yearly pricing
|
||||
3. Save the price IDs
|
||||
4. Add webhook endpoint: `https://your-api.com/api/v1/stripe/webhook`
|
||||
5. Copy webhook signing secret
|
||||
|
||||
### 4. Set Environment Variables
|
||||
|
||||
**API (`.env`):**
|
||||
```env
|
||||
STRIPE_SECRET_KEY=sk_test_xxxxx
|
||||
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
|
||||
SUPABASE_SERVICE_ROLE_KEY=your_key
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
**Frontend (`apps/main/.env`):**
|
||||
```env
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx
|
||||
VITE_STRIPE_STANDARD_MONTHLY_PRICE_ID=price_xxxxx
|
||||
```
|
||||
|
||||
### 5. Add Subscription Card to Settings
|
||||
|
||||
In `apps/main/src/pages/settings.tsx`:
|
||||
|
||||
```typescript
|
||||
import { SubscriptionCard } from "../components/SubscriptionCard";
|
||||
|
||||
// Inside your settings page JSX, add:
|
||||
<SubscriptionCard />
|
||||
```
|
||||
|
||||
## 🎯 How It Works
|
||||
|
||||
### User Journey
|
||||
|
||||
1. **Free User** sees upgrade prompt in SubscriptionCard
|
||||
2. Clicks "Passer à Standard"
|
||||
3. Redirected to Stripe Checkout
|
||||
4. Completes payment
|
||||
5. Redirected back to settings with `?success=true`
|
||||
6. Webhook updates database
|
||||
7. `user.is_paying` becomes `true`
|
||||
8. `user.subscription_tier` becomes `'standard'`
|
||||
9. UI automatically updates
|
||||
|
||||
### Data Sync
|
||||
|
||||
```
|
||||
Stripe → Webhook → API → Database → Frontend
|
||||
↓
|
||||
profiles.is_paying = true
|
||||
```
|
||||
|
||||
## 🎨 Using Subscription Card
|
||||
|
||||
The `SubscriptionCard` component shows:
|
||||
|
||||
**For Free Users:**
|
||||
- "Plan Gratuit" badge
|
||||
- "Passer à Standard" button
|
||||
- Initiates Stripe Checkout
|
||||
|
||||
**For Paying Users:**
|
||||
- "Actif" badge
|
||||
- Current subscription details
|
||||
- Renewal date
|
||||
- "Gérer l'abonnement" button (opens Stripe portal)
|
||||
- "Annuler" button
|
||||
|
||||
**For Canceling Users:**
|
||||
- "Annulation en cours" warning
|
||||
- Access until period end
|
||||
- "Réactiver l'abonnement" button
|
||||
|
||||
## 🔍 Check Payment Status Anywhere
|
||||
|
||||
```typescript
|
||||
import { useUser } from '../providers/UserStoreProvider';
|
||||
|
||||
function PremiumFeature() {
|
||||
const user = useUser();
|
||||
|
||||
if (!user.is_paying) {
|
||||
return <UpgradePrompt />;
|
||||
}
|
||||
|
||||
return <PremiumContent />;
|
||||
}
|
||||
```
|
||||
|
||||
## 🎣 Available Hooks
|
||||
|
||||
```typescript
|
||||
// Get subscription details
|
||||
const { data: subscription } = useSubscription();
|
||||
|
||||
// Check if paying (boolean)
|
||||
const { data: isPaying } = useIsPayingUser();
|
||||
|
||||
// Get available prices
|
||||
const { data: prices } = useStripePrices();
|
||||
|
||||
// Create checkout session
|
||||
const { mutate: checkout } = useCreateCheckoutSession();
|
||||
checkout({ priceId: 'price_xxxxx' });
|
||||
|
||||
// Open customer portal
|
||||
const { mutate: portal } = useCreatePortalSession();
|
||||
portal();
|
||||
|
||||
// Cancel subscription
|
||||
const { mutate: cancel } = useCancelSubscription();
|
||||
cancel();
|
||||
|
||||
// Reactivate subscription
|
||||
const { mutate: reactivate } = useReactivateSubscription();
|
||||
reactivate();
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Test Cards
|
||||
```
|
||||
Success: 4242 4242 4242 4242
|
||||
Decline: 4000 0000 0000 0002
|
||||
```
|
||||
|
||||
### Test Webhooks Locally
|
||||
```bash
|
||||
stripe listen --forward-to http://localhost:3000/api/v1/stripe/webhook
|
||||
```
|
||||
|
||||
### Trigger Events
|
||||
```bash
|
||||
stripe trigger customer.subscription.created
|
||||
stripe trigger customer.subscription.updated
|
||||
stripe trigger customer.subscription.deleted
|
||||
```
|
||||
|
||||
## 🔐 Security Features
|
||||
|
||||
✅ Webhook signature verification
|
||||
✅ Row Level Security on all tables
|
||||
✅ Users can only see their own subscriptions
|
||||
✅ API keys never exposed to frontend
|
||||
✅ Service role for database operations
|
||||
|
||||
## 📊 Database Schema
|
||||
|
||||
### Tables
|
||||
- `stripe_customers` - Links Stripe customers to users
|
||||
- `stripe_subscriptions` - Subscription records
|
||||
- `stripe_products` - Product catalog
|
||||
- `stripe_prices` - Pricing information
|
||||
|
||||
### Profile Fields
|
||||
- `is_paying: boolean` - Quick check for payment status
|
||||
- `subscription_tier: 'free' | 'standard'` - Current tier
|
||||
|
||||
### Functions
|
||||
- `is_paying_user(uuid)` - Returns boolean
|
||||
- `get_user_subscription_status(uuid)` - Returns subscription details
|
||||
- `get_user_stripe_customer_id(uuid)` - Returns Stripe customer ID
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
**Webhooks not working?**
|
||||
1. Check API logs
|
||||
2. Verify webhook secret in `.env`
|
||||
3. Test with Stripe CLI
|
||||
4. Check Stripe Dashboard → Webhooks for delivery status
|
||||
|
||||
**Subscription not showing?**
|
||||
1. Check `stripe_subscriptions` table in Supabase
|
||||
2. Verify webhook was received
|
||||
3. Check `profiles.is_paying` field
|
||||
4. Ensure customer has `user_id` in metadata
|
||||
|
||||
**Can't create checkout?**
|
||||
1. Verify `STRIPE_SECRET_KEY` is set
|
||||
2. Check `VITE_STRIPE_PUBLISHABLE_KEY` in frontend
|
||||
3. Ensure price ID is correct
|
||||
4. Check browser console for errors
|
||||
|
||||
## 📞 Support
|
||||
|
||||
See detailed docs:
|
||||
- **Setup Guide**: `docs/STRIPE_SETUP.md`
|
||||
- **Technical Details**: `docs/STRIPE_IMPLEMENTATION_SUMMARY.md`
|
||||
- **Quick Reference**: `docs/STRIPE_QUICK_REFERENCE.md`
|
||||
|
||||
## 🎉 Next Steps
|
||||
|
||||
1. ✅ Database schema created
|
||||
2. ✅ API endpoints implemented
|
||||
3. ✅ Frontend hooks ready
|
||||
4. ✅ UI component created
|
||||
5. ⏳ Run migrations
|
||||
6. ⏳ Configure Stripe
|
||||
7. ⏳ Set environment variables
|
||||
8. ⏳ Test with test cards
|
||||
9. ⏳ Add SubscriptionCard to settings page
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ Implementation Complete
|
||||
**Architecture**: Node.js API + Supabase Database
|
||||
**Plan**: Single "Standard" tier
|
||||
**Ready to Deploy**: Yes, follow setup guide
|
||||
|
||||
359
docs/STRIPE_SETUP.md
Normal file
359
docs/STRIPE_SETUP.md
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
# Stripe Integration Setup Guide
|
||||
|
||||
This guide walks you through setting up Stripe payments integration for Xtablo with a single "Standard" plan.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Prerequisites](#prerequisites)
|
||||
2. [Database Setup](#database-setup)
|
||||
3. [Stripe Configuration](#stripe-configuration)
|
||||
4. [Backend API Setup](#backend-api-setup)
|
||||
5. [Frontend Integration](#frontend-integration)
|
||||
6. [Testing](#testing)
|
||||
7. [Usage Examples](#usage-examples)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Supabase project with database access
|
||||
- Stripe account (test mode for development)
|
||||
- Supabase Wrappers extension enabled
|
||||
- Node.js API already running
|
||||
|
||||
## Database Setup
|
||||
|
||||
### 1. Enable Wrappers Extension
|
||||
|
||||
In your Supabase SQL Editor, enable the Wrappers extension:
|
||||
|
||||
```sql
|
||||
create extension if not exists wrappers with schema extensions;
|
||||
```
|
||||
|
||||
### 2. Store Stripe API Key in Vault
|
||||
|
||||
```sql
|
||||
select vault.create_secret(
|
||||
'sk_test_xxxxx', -- Your Stripe secret key (test or live)
|
||||
'stripe',
|
||||
'Stripe API key for Wrappers'
|
||||
);
|
||||
```
|
||||
|
||||
**Note the `key_id` returned** - you'll need this for the next step.
|
||||
|
||||
### 3. Run Migration Scripts
|
||||
|
||||
Execute the SQL migration files in order:
|
||||
|
||||
```bash
|
||||
# From your Supabase SQL Editor
|
||||
1. Run: sql/35_stripe_wrappers.sql
|
||||
2. Run: sql/36_stripe_webhooks.sql
|
||||
```
|
||||
|
||||
### 4. Update API Key ID
|
||||
|
||||
In `35_stripe_wrappers.sql`, update line 27 with your actual `key_id` from step 2:
|
||||
|
||||
```sql
|
||||
create server stripe_server
|
||||
foreign data wrapper stripe_wrapper
|
||||
options (
|
||||
api_key_id 'YOUR_KEY_ID_HERE', -- Update this!
|
||||
api_url 'https://api.stripe.com/v1/',
|
||||
api_version '2024-06-20'
|
||||
);
|
||||
```
|
||||
|
||||
## Stripe Configuration
|
||||
|
||||
### 1. Create Products and Prices
|
||||
|
||||
In your Stripe Dashboard:
|
||||
|
||||
1. Go to **Products**
|
||||
2. Click **Add product**
|
||||
3. Create your subscription tiers (e.g., "Premium", "Pro")
|
||||
4. Add pricing (monthly, yearly, etc.)
|
||||
5. Note the `price_id` (starts with `price_xxx`)
|
||||
|
||||
### 2. Create the "Standard" Plan
|
||||
|
||||
In your Stripe Dashboard:
|
||||
|
||||
1. Go to **Products**
|
||||
2. Click **Add product**
|
||||
3. Name: **"Standard"** (important: must match exactly)
|
||||
4. Add your pricing:
|
||||
- Monthly: e.g., €9.99/month
|
||||
- Yearly: e.g., €99/year (optional)
|
||||
5. **Save the price IDs** (e.g., `price_xxxxx`)
|
||||
|
||||
### 3. Set up Webhooks
|
||||
|
||||
1. Go to **Developers → Webhooks** in Stripe Dashboard
|
||||
2. Click **Add endpoint**
|
||||
3. URL: `https://YOUR_API_URL/api/v1/stripe/webhook`
|
||||
4. Select events to listen to:
|
||||
- `customer.created`
|
||||
- `customer.updated`
|
||||
- `customer.deleted`
|
||||
- `customer.subscription.created`
|
||||
- `customer.subscription.updated`
|
||||
- `customer.subscription.deleted`
|
||||
- `product.created`
|
||||
- `product.updated`
|
||||
- `product.deleted`
|
||||
- `price.created`
|
||||
- `price.updated`
|
||||
- `price.deleted`
|
||||
5. **Copy the webhook signing secret** (starts with `whsec_`)
|
||||
|
||||
## Backend API Setup
|
||||
|
||||
The backend API handlers are already implemented in:
|
||||
|
||||
- `api/src/stripe.ts` - Main Stripe routes
|
||||
- `api/src/stripe-webhook.ts` - Webhook handler
|
||||
- `api/src/routers.ts` - Router configuration
|
||||
|
||||
### 1. Install Stripe SDK
|
||||
|
||||
```bash
|
||||
cd api
|
||||
npm install stripe @stripe/stripe-js
|
||||
```
|
||||
|
||||
### 2. Configure Environment Variables
|
||||
|
||||
Add to your `.env` file:
|
||||
|
||||
```env
|
||||
# Stripe
|
||||
STRIPE_SECRET_KEY=sk_test_xxxxx
|
||||
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
|
||||
|
||||
# For webhooks to work
|
||||
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
|
||||
```
|
||||
|
||||
### 3. Restart API Server
|
||||
|
||||
The Stripe routes are automatically registered at:
|
||||
|
||||
- `POST /api/v1/stripe/webhook` - Webhook endpoint
|
||||
- `POST /api/v1/stripe/create-checkout-session` - Create checkout
|
||||
- `POST /api/v1/stripe/create-portal-session` - Customer portal
|
||||
- `GET /api/v1/stripe/subscription` - Get subscription status
|
||||
- `GET /api/v1/stripe/is-paying` - Check if paying
|
||||
- `GET /api/v1/stripe/prices` - Get available prices
|
||||
- `POST /api/v1/stripe/cancel-subscription` - Cancel subscription
|
||||
- `POST /api/v1/stripe/reactivate-subscription` - Reactivate subscription
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
The frontend hooks are already implemented in `apps/main/src/hooks/stripe.ts`.
|
||||
|
||||
### 1. Install Stripe SDK
|
||||
|
||||
```bash
|
||||
cd apps/main
|
||||
npm install @stripe/stripe-js
|
||||
```
|
||||
|
||||
### 2. Configure Environment Variable
|
||||
|
||||
Add to `apps/main/.env`:
|
||||
|
||||
```env
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx
|
||||
```
|
||||
|
||||
### 3. Available Hooks
|
||||
|
||||
All hooks are ready to use:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
useSubscription, // Get subscription details
|
||||
useIsPayingUser, // Check if user is paying
|
||||
useStripePrices, // Get available prices
|
||||
useCreateCheckoutSession, // Start checkout flow
|
||||
useCreatePortalSession, // Open customer portal
|
||||
useCancelSubscription, // Cancel subscription
|
||||
useReactivateSubscription, // Reactivate canceled subscription
|
||||
} from "../hooks/stripe";
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Check if User is Paying
|
||||
|
||||
```typescript
|
||||
// In any component
|
||||
const { data: isPaying } = useIsPayingUser();
|
||||
|
||||
if (!isPaying) {
|
||||
return <UpgradePrompt />;
|
||||
}
|
||||
```
|
||||
|
||||
### Get Subscription Details
|
||||
|
||||
```typescript
|
||||
const { data: subscription } = useSubscription();
|
||||
|
||||
if (subscription) {
|
||||
console.log("Status:", subscription.status);
|
||||
console.log("Plan:", subscription.product_name);
|
||||
console.log("Renews:", subscription.current_period_end);
|
||||
}
|
||||
```
|
||||
|
||||
### Access Subscription Status from Profile
|
||||
|
||||
```typescript
|
||||
// The user profile already has is_paying and subscription_tier fields
|
||||
const user = useUser();
|
||||
|
||||
if (user.is_paying) {
|
||||
console.log("User is on:", user.subscription_tier);
|
||||
}
|
||||
```
|
||||
|
||||
### Create Checkout Session
|
||||
|
||||
```typescript
|
||||
import { useCreateCheckoutSession } from "../hooks/stripe";
|
||||
|
||||
function UpgradeButton() {
|
||||
const { mutate: createCheckout, isPending } = useCreateCheckoutSession();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() =>
|
||||
createCheckout({
|
||||
priceId: "price_xxxxx", // Your Standard plan price ID
|
||||
})
|
||||
}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Loading..." : "Subscribe to Standard"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Manage Subscription (Customer Portal)
|
||||
|
||||
```typescript
|
||||
import { useCreatePortalSession } from "../hooks/stripe";
|
||||
|
||||
function ManageSubscriptionButton() {
|
||||
const { mutate: openPortal, isPending } = useCreatePortalSession();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => openPortal(window.location.href)}
|
||||
disabled={isPending}
|
||||
>
|
||||
Manage Subscription
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Cancel Subscription
|
||||
|
||||
```typescript
|
||||
import { useCancelSubscription } from "../hooks/stripe";
|
||||
|
||||
function CancelButton() {
|
||||
const { mutate: cancel, isPending } = useCancelSubscription();
|
||||
|
||||
return (
|
||||
<button onClick={() => cancel()} disabled={isPending}>
|
||||
Cancel Subscription
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### 1. Use Stripe Test Mode
|
||||
|
||||
Use test cards from [Stripe Testing](https://stripe.com/docs/testing):
|
||||
|
||||
- Success: `4242 4242 4242 4242`
|
||||
- Decline: `4000 0000 0000 0002`
|
||||
|
||||
### 2. Test Webhooks Locally
|
||||
|
||||
```bash
|
||||
# Install Stripe CLI
|
||||
brew install stripe/stripe-cli/stripe
|
||||
|
||||
# Login
|
||||
stripe login
|
||||
|
||||
# Forward webhooks to local API
|
||||
stripe listen --forward-to http://localhost:3000/api/v1/stripe/webhook
|
||||
```
|
||||
|
||||
### 3. Trigger Test Events
|
||||
|
||||
```bash
|
||||
stripe trigger customer.subscription.created
|
||||
stripe trigger customer.subscription.updated
|
||||
stripe trigger customer.subscription.deleted
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Stripe API keys stored in Supabase Vault
|
||||
- [ ] Webhook signing secret configured
|
||||
- [ ] Row Level Security (RLS) enabled on all tables
|
||||
- [ ] Edge Function uses service role key
|
||||
- [ ] Customer metadata includes user_id for mapping
|
||||
- [ ] Test mode used for development
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Webhooks Not Working
|
||||
|
||||
1. Check webhook endpoint URL is correct (`https://your-api/api/v1/stripe/webhook`)
|
||||
2. Verify signing secret matches in `.env`
|
||||
3. Check API logs for webhook errors
|
||||
4. Ensure `SUPABASE_SERVICE_ROLE_KEY` is set in API environment
|
||||
5. Test with Stripe CLI: `stripe trigger customer.subscription.created`
|
||||
|
||||
### Subscription Not Showing
|
||||
|
||||
1. Verify customer has `user_id` in metadata
|
||||
2. Check webhook was received (Stripe Dashboard → Webhooks)
|
||||
3. Query `stripe_subscriptions` table directly
|
||||
4. Check `profiles.is_paying` was updated by trigger
|
||||
|
||||
### Foreign Tables Empty
|
||||
|
||||
1. Verify Stripe API key is correct
|
||||
2. Check key_id in server configuration
|
||||
3. Test with: `SELECT * FROM stripe.customers LIMIT 5;`
|
||||
4. Ensure Wrappers extension is enabled
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Create pricing page component
|
||||
2. Build subscription management UI
|
||||
3. Implement usage-based billing (if needed)
|
||||
4. Add customer portal for self-service
|
||||
5. Set up email notifications for billing events
|
||||
|
||||
---
|
||||
|
||||
For more information, see:
|
||||
|
||||
- [Supabase Stripe Integration](https://supabase.com/docs/guides/integrations/stripe)
|
||||
- [Stripe Documentation](https://stripe.com/docs)
|
||||
- [Supabase Wrappers](https://supabase.github.io/wrappers/)
|
||||
Loading…
Reference in a new issue