Agent Skills: Stripe Integration Expert

>-

UncategorizedID: azlekov/my-claude-code/stripe

Install this agent skill to your local

pnpm dlx add-skill https://github.com/azlekov/my-claude-code/tree/HEAD/skills/stripe

Skill Files

Browse the full folder contents for stripe.

Download Skill

Loading file tree…

skills/stripe/SKILL.md

Skill Metadata

Name
stripe
Description
>-

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-based
  • references/webhooks.md - Signature verification, event handling, idempotency
  • references/nextjs-integration.md - Complete Next.js patterns
  • references/mobile-integration.md - iOS, Android, Flutter integration
  • references/supabase-sync.md - Database schema, sync patterns, RLS

Common Mistakes to Avoid

  1. Parsing body before signature verification - Use raw text body
  2. Not implementing idempotency - Events can be sent multiple times
  3. Exposing secret keys in client code - Use publishable keys only
  4. Collecting card numbers directly - Always use Stripe.js/Elements
  5. Not handling subscription status changes - Sync via webhooks
  6. Hardcoding prices - Use Stripe Dashboard or API for prices
  7. Not testing webhooks locally - Use stripe listen --forward-to
  8. 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