<quick_start> Add payments to a Next.js + Supabase project:
- Install Stripe:
npm install stripe @stripe/stripe-js - Add env vars (see quick_reference below)
- Create idempotency table (see schema below)
- Choose workflow:
setup-new-project.mdoradd-webhook-handler.md
// Lazy-loaded Stripe client
import Stripe from 'stripe';
let _stripe: Stripe | null = null;
export function getStripe(): Stripe {
if (!_stripe) {
_stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2025-12-15.clover' });
}
return _stripe;
}</quick_start>
<success_criteria> Integration is successful when:
- Webhook handler uses database-backed idempotency (not in-memory)
- All keys in environment variables (never hardcoded)
- Test mode fully working before any live mode deployment
- Signature verification on all webhook endpoints
- Event logging before processing (insert-before-process pattern)
- Go-live checklist completed before production deployment </success_criteria>
<essential_principles>
Core Principles
- Idempotency is Non-Negotiable
- ALL webhook handlers MUST use database-backed idempotency - Never use in-memory Sets (lost on serverless cold starts) - Insert event record BEFORE processing, not after
- Test/Live Mode Separation
- Use environment variables for ALL keys (never hardcode) - Test keys: sk_test_, pk_test_, whsec_test_ - Live keys: sk_live_, pk_live_, whsec_live_ - Products/prices must be recreated in live mode
- Shared Stripe Account
- All NetZero Suite projects share ONE Stripe account - Same webhook secret can be used across projects - Each project has its own webhook endpoint URL
- Lazy Client Initialization
- Never initialize Stripe at module level (build errors) - Use factory function pattern for server-side client - Check for API key before creating instance
</essential_principles>
What Are You Building?
Before proceeding, identify your use case:
| Use Case | Workflow | Description |
|---|---|---|
| New project | setup-new-project.md | Fresh Stripe integration from scratch |
| Add webhooks | add-webhook-handler.md | Add webhook handler to existing project |
| Subscriptions | implement-subscriptions.md | Recurring billing with plans |
| Credit system | add-credit-system.md | Pay-as-you-go credits |
| Go live | go-live-checklist.md | Test → Production migration |
Workflow Routing
If setting up Stripe in a new project: → Read workflows/setup-new-project.md → Then read reference/environment-vars.md → Use templates/stripe-client.ts and templates/env-example.txt
If adding webhook handling: → Read workflows/add-webhook-handler.md → Then read reference/webhook-patterns.md → Use templates/webhook-handler-nextjs.ts and templates/idempotency-migration.sql
If implementing subscription billing: → Read workflows/implement-subscriptions.md → Then read reference/pricing-models.md → Use templates/plans-config.ts
If adding credit/usage-based system: → Read workflows/add-credit-system.md → Then read reference/pricing-models.md
If migrating test → production: → Read workflows/go-live-checklist.md
<quick_reference>
Quick Reference
Environment Variables (Standard)
# Server-side (never expose to client)
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Client-side (safe to expose)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# Optional: Price IDs (for test→live switching)
STRIPE_PRICE_STARTER_MONTHLY=price_...
STRIPE_PRICE_PRO_MONTHLY=price_...Common Webhook Events
| Event | When It Fires | Action |
|---|---|---|
checkout.session.completed | Customer completes checkout | Create subscription record |
customer.subscription.created | New subscription starts | Initialize user limits |
customer.subscription.updated | Plan change, renewal | Update plan/limits |
customer.subscription.deleted | Cancellation | Downgrade to free |
invoice.paid | Monthly renewal success | Reset usage counters |
invoice.payment_failed | Payment failed | Mark as past_due |
Stripe Client Pattern
let _stripe: Stripe | null = null;
export function getStripe(): Stripe {
if (!_stripe) {
const key = process.env.STRIPE_SECRET_KEY;
if (!key) throw new Error('STRIPE_SECRET_KEY not configured');
_stripe = new Stripe(key, {
apiVersion: '2025-12-15.clover',
typescript: true
});
}
return _stripe;
}Idempotency Table Schema
CREATE TABLE stripe_webhook_events (
id TEXT PRIMARY KEY, -- Use Stripe event ID directly
type TEXT NOT NULL, -- Event type
data JSONB NOT NULL, -- Full event payload
processed_at TIMESTAMPTZ DEFAULT NOW()
);Webhook Handler Structure
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get('stripe-signature');
// 1. Verify signature
const event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
// 2. Check idempotency (BEFORE processing)
const { data: existing } = await supabase
.from('stripe_webhook_events')
.select('id')
.eq('id', event.id)
.single();
if (existing) return NextResponse.json({ duplicate: true });
// 3. Log event (INSERT before processing)
await supabase.from('stripe_webhook_events').insert({
id: event.id,
type: event.type,
data: event,
});
// 4. Process event
switch (event.type) {
case 'checkout.session.completed':
await handleCheckout(event.data.object);
break;
// ... other handlers
}
return NextResponse.json({ received: true });
}</quick_reference>
<integration_notes>
Integration Notes
Works With
- Supabase: Use service role client for webhook handlers (bypasses RLS)
- Prisma: Alternative to Supabase for idempotency table
- Vercel: Add runtime/maxDuration config for webhook routes
- Next.js App Router: Use
request.text()for raw body
Related Skills
supabase-sql-skill- For database migrationscreate-hooks-skill- For post-deployment notifications
GitHub Repository
Private templates and examples available at: github.com/ScientiaCapital/stripe-stack
</integration_notes>
<reference_index>
Reference Files
| File | Purpose |
|---|---|
reference/webhook-patterns.md | Idempotency, event handling, error recovery |
reference/pricing-models.md | Plans vs Credits vs Usage-based billing |
reference/environment-vars.md | Standard env var conventions |
reference/common-errors.md | Troubleshooting guide |
Template Files
| File | Purpose |
|---|---|
templates/webhook-handler-nextjs.ts | Complete webhook route (copy-paste) |
templates/stripe-client.ts | Lazy-loaded client factory |
templates/plans-config.ts | Subscription plan definitions |
templates/idempotency-migration.sql | Supabase migration |
templates/webhook-handler.test.ts | Test template |
templates/env-example.txt | Standard.env template |
Workflow Files
| File | Purpose |
|---|---|
workflows/setup-new-project.md | Fresh Stripe integration |
workflows/add-webhook-handler.md | Add webhook to existing project |
workflows/implement-subscriptions.md | Subscription billing |
workflows/add-credit-system.md | Pay-as-you-go credits |
workflows/go-live-checklist.md | Test → Production migration |
</reference_index>
Emit Outcome Sidecar
As the final step, write to ~/.claude/skill-analytics/last-outcome-stripe-stack.json:
{"ts":"[UTC ISO8601]","skill":"stripe-stack","version":"1.0.0","variant":"default",
"status":"[success|partial|error]","runtime_ms":[estimated ms from start],
"metrics":{"webhooks_configured":[n],"products_created":[n],"checkout_flows_built":[n]},
"error":null,"session_id":"[YYYY-MM-DD]"}Use status "partial" if some stages failed but results were produced. Use "error" only if no output was generated.