11 KiB
Stripe Integration Implementation Summary
Overview
Complete Stripe integration for Xtablo with a single "Standard" subscription plan.
Architecture:
- ✅ Webhook-based: Stripe webhooks sync data to Supabase
- ✅ Direct Supabase access: Frontend queries Supabase directly (no API for reads)
- ✅ API for actions: Checkout and subscription management via Node.js API
- ✅ RLS-protected: Row Level Security ensures data privacy
✅ What Has Been Implemented
1. Database Schema (sql/35_stripe_wrappers.sql)
Tables (Synced via Webhooks)
public.stripe_customers- Customer records with user mappingpublic.stripe_subscriptions- Subscription historypublic.stripe_products- Product catalogpublic.stripe_prices- Pricing information
Profile Enhancements
profiles.is_paying- Boolean flag for quick checksprofiles.subscription_tier- Current tier ('free' or 'standard')
Helper Functions
-- 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_payingwhen subscriptions change updated_attimestamps 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 (signature verified)POST /api/v1/stripe/create-checkout-session- Start subscription checkoutPOST /api/v1/stripe/create-portal-session- Open customer portalPOST /api/v1/stripe/cancel-subscription- Cancel at period endPOST /api/v1/stripe/reactivate-subscription- Reactivate subscription
Note: Subscription status queries (is-paying, subscription, prices) are handled directly by the frontend using Supabase client with RLS policies.
Features:
- ✅ Automatic customer creation with user mapping
- ✅ Secure webhook signature verification
- ✅ Complete subscription lifecycle management
- ✅ Customer portal access
- ✅ Subscription cancellation/reactivation
- ✅ Optimistic updates (API updates DB before webhook for instant UI feedback)
4. Frontend Hooks (apps/main/src/hooks/stripe.ts)
Available Hooks:
Direct Supabase Queries (Fast, RLS-protected):
useSubscription()- Get subscription details from SupabaseuseIsPayingUser()- Check if user is paying (from user profile)useStripePrices()- Get available prices from Supabase
API Calls (For Actions):
useCreateCheckoutSession()- Start checkout (calls API)useCreatePortalSession()- Open portal (calls API)useCancelSubscription()- Cancel subscription (calls API)useReactivateSubscription()- Reactivate subscription (calls API)
5. TypeScript Types (packages/shared/src/types/stripe.types.ts)
Complete type definitions:
StripeCustomerStripeSubscriptionStripeProductStripePrice- 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)
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 Hook (Same as Profile)
const { data: isPaying } = useIsPayingUser();
// Returns user.is_paying directly
Method 3: Direct Supabase Query
const { data: subscription } = await supabase
.from("stripe_subscriptions")
.select("*")
.eq("user_id", userId)
.in("status", ["active", "trialing"])
.single();
const isPaying = !!subscription;
🔄 Data Flow
Subscription Creation Flow
-
User Clicks "Subscribe to Standard" → Frontend calls
useCreateCheckoutSession({ priceId })→ Calls API:POST /api/v1/stripe/create-checkout-session -
API Creates Checkout → Creates/retrieves Stripe customer (with
user_idin metadata) → Creates Stripe checkout session → Returns checkout URL → Frontend redirects to Stripe -
User Completes Payment → Stripe processes payment → Fires
customer.subscription.createdwebhook -
Webhook Received → API endpoint
POST /api/v1/stripe/webhookreceives event → Verifies signature → Callshandle_stripe_subscription_upsert()database function -
Database Updated → Function inserts into
stripe_subscriptions→ Trigger fires:update_profile_on_subscription_change→ Updatesprofiles.is_paying = trueandsubscription_tier = 'standard' -
Frontend Queries Supabase →
useSubscription()queriesstripe_subscriptionstable directly →useUser()shows updatedis_payingandsubscription_tier→ UI automatically reflects new status
Read Flow (No API Needed)
Frontend → Supabase Client → RLS Policies → stripe_subscriptions table → User's data
Benefits:
- ⚡ Fast: Direct database access, no API hop
- 🔒 Secure: RLS ensures users only see their data
- 📊 Real-time: Can use Supabase realtime subscriptions if needed
🚀 Next Steps to Complete Implementation
Required Setup:
-
Update Vault Key ID
- Get key_id after storing Stripe API key
- Update in
sql/35_stripe_wrappers.sqlline 27
-
Install Stripe SDK
cd api npm install stripe @stripe/stripe-js -
Configure API Environment Variables Add to
api/.env:STRIPE_SECRET_KEY=sk_test_xxxxx STRIPE_WEBHOOK_SECRET=whsec_xxxxx SUPABASE_SERVICE_ROLE_KEY=your_service_role_key -
Configure Frontend Environment Add to
apps/main/.env:VITE_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx -
Create "Standard" Plan in Stripe
- Create product named exactly "Standard"
- Add pricing (monthly/yearly)
- Note price IDs for frontend
-
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
- Add endpoint:
Frontend Implementation:
-
Hooks Already Created ✅
useSubscription()- Get subscription detailsuseIsPayingUser()- Check payment statususeCreateCheckoutSession()- Initiate checkoutuseCreatePortalSession()- Customer portaluseCancelSubscription()- Cancel subscriptionuseReactivateSubscription()- Reactivate
-
Build UI Components
- Pricing page
- Subscription management page
- Payment status badges
- Upgrade prompts
-
Add Feature Gates
if (!user.is_paying) { return <PremiumFeatureLockedPrompt />; }
📝 Environment Variables Needed
API (api/.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)
# 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_payingupdates 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
- Direct Stripe Access - Query Stripe data without API calls
- Instant Status Checks -
is_payingflag on user profile - Automatic Sync - Webhooks keep data current
- Secure - RLS ensures data privacy
- Type-Safe - Full TypeScript support
- Scalable - Handles thousands of subscriptions
- 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