stripe
SKILL.md
Stripe Integration Expert
Comprehensive guidance for integrating Stripe payments including subscriptions, usage-based billing, webhooks, and multi-platform support (Next.js, iOS, Android, Flutter) with Supabase database synchronization.
Philosophy: Use Stripe Checkout for simplicity, Payment Intents for customization. Always verify webhooks. For Supabase sync, prefer Stripe Sync Engine (one-click, zero maintenance) unless you need custom schemas. Never expose secret keys to clients.
Critical Rules
API Keys Security
| Key Type | Prefix | Safety | Use Case |
|---|---|---|---|
| Publishable | pk_live_ / pk_test_ |
Client-safe | Browser, mobile apps |
| Secret | sk_live_ / sk_test_ |
Backend ONLY | Servers, Edge Functions |
| Restricted | rk_live_ / rk_test_ |
Backend ONLY | Limited permissions |
| Webhook Secret | whsec_ |
Backend ONLY | Signature verification |
NEVER:
- Expose secret keys in client code
- Commit keys to version control
- Log full API keys
- Use secret keys in browser/mobile apps
ALWAYS:
- Use environment variables for all keys
- Use publishable key for client-side Stripe.js
- Use restricted keys for specific operations
- Rotate keys immediately if compromised
Webhook Security (CRITICAL)
// CORRECT: Use raw body for signature verification
const body = await req.text() // Raw string, NOT parsed
const signature = req.headers.get('stripe-signature')!
const event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
// WRONG: This will FAIL signature verification
const body = await req.json() // DON'T parse first!
Webhook Rules:
- ALWAYS verify signatures before processing
- Use raw request body (NOT parsed JSON)
- Implement idempotency (track processed events)
- Return 200 quickly, process asynchronously
- Handle retries gracefully
PCI Compliance
- NEVER collect card numbers directly on your server
- ALWAYS use Stripe.js, Elements, or Checkout
- Use Payment Intents or Checkout Sessions
- Enable SCA for EU customers (automatic with Checkout)
- Log payment events but NEVER log card details
Quick Reference
Subscription Billing Models
| Model | Use Case | Implementation |
|---|---|---|
| Flat Rate | Fixed monthly/yearly | Single price, licensed |
| Per-Seat | Per user pricing | quantity on subscription |
| Usage-Based | Pay for consumption | Meters + metered billing |
| Tiered | Volume discounts | tiered pricing |
| Hybrid | Base + usage | Multiple prices on subscription |
Essential Webhook Events
| Event | When | Action |
|---|---|---|
checkout.session.completed |
Successful checkout | Provision access |
customer.subscription.created |
New subscription | Create local record |
customer.subscription.updated |
Plan change/renewal | Update local record |
customer.subscription.deleted |
Cancellation | Revoke access |
invoice.paid |
Successful payment | Update billing status |
invoice.payment_failed |
Payment failure | Notify customer |
Environment Variables
# .env.local (Next.js)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Supabase Edge Functions
# Set via: supabase secrets set STRIPE_SECRET_KEY=sk_test_...
Workflow Decision Tree
User mentions Stripe/payments?
├─> Setting up Stripe?
│ └─> Use: Initial Setup (below)
├─> Creating subscriptions?
│ ├─> Simple flat rate?
│ │ └─> Use: Checkout Session pattern
│ ├─> Per-seat pricing?
│ │ └─> See: references/subscriptions.md
│ └─> Usage-based/metered?
│ └─> See: references/subscriptions.md
├─> Handling webhooks?
│ └─> See: references/webhooks.md
├─> Next.js integration?
│ └─> See: references/nextjs-integration.md
├─> Mobile integration?
│ └─> See: references/mobile-integration.md
├─> Syncing with Supabase?
│ ├─> Zero-maintenance sync? (Recommended)
│ │ └─> Use: Stripe Sync Engine (references/supabase-sync.md)
│ ├─> Custom schema/transformations?
│ │ └─> Use: Webhook Sync (references/supabase-sync.md)
│ └─> Real-time admin queries?
│ └─> Use: Stripe Wrapper FDW (references/supabase-sync.md)
└─> MRR/Revenue analytics?
└─> See: references/supabase-sync.md (Business Analytics Queries)
Initial Setup
1. Install Dependencies
Next.js:
npm install stripe @stripe/stripe-js
iOS (Swift Package Manager):
https://github.com/stripe/stripe-ios
Android (Gradle):
implementation("com.stripe:stripe-android:20.+")
Flutter:
dependencies:
flutter_stripe: ^11.0.0
2. Initialize Stripe
Server (Next.js):
// lib/stripe/server.ts
import Stripe from 'stripe'
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia',
typescript: true,
})
Client (Next.js):
// lib/stripe/client.ts
import { loadStripe, Stripe } from '@stripe/stripe-js'
let stripePromise: Promise<Stripe | null>
export function getStripe() {
if (!stripePromise) {
stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!)
}
return stripePromise
}
Checkout Session Quick Reference
Server Action (Next.js):
'use server'
import { redirect } from 'next/navigation'
import { stripe } from '@/lib/stripe/server'
import { createClient } from '@/lib/supabase/server'
export async function createCheckoutSession(priceId: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
// Get or create Stripe customer
const { data: customer } = await supabase
.from('customers')
.select('stripe_customer_id')
.eq('user_id', user.id)
.single()
const session = await stripe.checkout.sessions.create({
customer: customer?.stripe_customer_id,
customer_email: !customer ? user.email : undefined,
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
metadata: {
user_id: user.id,
},
})
redirect(session.url!)
}
Customer Portal Quick Reference
'use server'
import { redirect } from 'next/navigation'
import { stripe } from '@/lib/stripe/server'
import { createClient } from '@/lib/supabase/server'
export async function createPortalSession() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
const { data: customer } = await supabase
.from('customers')
.select('stripe_customer_id')
.eq('user_id', user.id)
.single()
if (!customer?.stripe_customer_id) {
redirect('/pricing')
}
const session = await stripe.billingPortal.sessions.create({
customer: customer.stripe_customer_id,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
})
redirect(session.url)
}
Webhook Handler Quick Reference
// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers'
import { stripe } from '@/lib/stripe/server'
import { createClient } from '@supabase/supabase-js'
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SECRET_KEY!
)
export async function POST(req: Request) {
const body = await req.text()
const headersList = await headers()
const signature = headersList.get('stripe-signature')!
let event
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err: any) {
console.error('Webhook signature verification failed:', err.message)
return new Response(`Webhook Error: ${err.message}`, { status: 400 })
}
// Idempotency check
const { data: existing } = await supabaseAdmin
.from('stripe_events')
.select('stripe_event_id')
.eq('stripe_event_id', event.id)
.single()
if (existing) {
return new Response(JSON.stringify({ received: true, cached: true }))
}
// Handle the event
try {
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutComplete(event.data.object)
break
case 'customer.subscription.created':
case 'customer.subscription.updated':
await handleSubscriptionChange(event.data.object)
break
case 'customer.subscription.deleted':
await handleSubscriptionDeleted(event.data.object)
break
case 'invoice.payment_failed':
await handlePaymentFailed(event.data.object)
break
}
// Record processed event
await supabaseAdmin
.from('stripe_events')
.insert({
stripe_event_id: event.id,
type: event.type,
})
} catch (err) {
console.error('Error processing webhook:', err)
return new Response('Webhook handler failed', { status: 500 })
}
return new Response(JSON.stringify({ received: true }))
}
Subscription Check in Proxy
// proxy.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function proxy(request: NextRequest) {
// ... Supabase client setup ...
const { data: { user } } = await supabase.auth.getUser()
// Check subscription for premium routes
if (user && request.nextUrl.pathname.startsWith('/dashboard')) {
const { data: subscription } = await supabase
.from('subscriptions')
.select('status, current_period_end')
.eq('customer_id', user.id)
.in('status', ['active', 'trialing'])
.single()
if (!subscription) {
return NextResponse.redirect(new URL('/pricing', request.url))
}
}
return supabaseResponse
}
Pre-Flight Checklist
Before ANY Stripe integration:
- Secret key in environment variables only
- Publishable key for client-side code
- Webhook secret configured
- Webhook signature verification implemented
- Idempotency handling for webhooks
- Using Stripe.js for card collection (PCI compliance)
- Test mode keys for development
- Customer portal configured in Stripe Dashboard
- Supabase tables created for sync
- RLS policies on Stripe-synced tables
- Error handling for all Stripe API calls
Resources
Reference Files (Load as needed)
references/subscriptions.md- Billing models, lifecycle, per-seat, usage-basedreferences/webhooks.md- Signature verification, event handling, idempotencyreferences/nextjs-integration.md- Complete Next.js patternsreferences/mobile-integration.md- iOS, Android, Flutter integrationreferences/supabase-sync.md- Database schema, sync patterns, RLS
Common Mistakes to Avoid
- Parsing body before signature verification - Use raw text body
- Not implementing idempotency - Events can be sent multiple times
- Exposing secret keys in client code - Use publishable keys only
- Collecting card numbers directly - Always use Stripe.js/Elements
- Not handling subscription status changes - Sync via webhooks
- Hardcoding prices - Use Stripe Dashboard or API for prices
- Not testing webhooks locally - Use
stripe listen --forward-to - Missing error handling - Stripe API can fail
Testing
Local Webhook Testing
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Forward webhooks to local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Trigger test events
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
Test Card Numbers
| Card | Number | Use Case |
|---|---|---|
| Success | 4242 4242 4242 4242 |
Successful payment |
| Decline | 4000 0000 0000 0002 |
Card declined |
| Auth Required | 4000 0025 0000 3155 |
3D Secure required |
| Insufficient Funds | 4000 0000 0000 9995 |
Insufficient funds |
Skill Version: 1.0.0 Last Updated: 2025-01-07 Documentation: https://docs.stripe.com
Weekly Installs
6
Repository
velcrafting/codex-skillsGitHub Stars
1
First Seen
Jan 24, 2026
Security Audits
Installed on
opencode4
gemini-cli4
codex4
codebuddy3
claude-code3
github-copilot3