payment-security-clerk-billing-stripe
关于
This skill enables secure payment integration using Clerk Billing and Stripe while ensuring PCI-DSS compliance by never handling raw card data. It helps developers implement subscription payments, Stripe Checkout, payment gating, and webhook handling. Use it when you need to add billing functionality without managing complex payment security requirements directly.
快速安装
Claude Code
推荐npx skills add harperaa/secure-claude-skills/plugin add https://github.com/harperaa/secure-claude-skillsgit clone https://github.com/harperaa/secure-claude-skills.git ~/.claude/skills/payment-security-clerk-billing-stripe在 Claude Code 中复制并粘贴此命令以安装该技能
技能文档
Payment Security - Clerk Billing + Stripe
Why We Don't Handle Payments Directly
PCI-DSS Compliance Requirements
If you store, process, or transmit credit card data, you must comply with Payment Card Industry Data Security Standard (PCI-DSS). Requirements include:
- Annual security audits ($20,000-$50,000)
- Quarterly vulnerability scans
- Secure network architecture
- Encryption of cardholder data
- Access control measures
- Regular security testing
Small companies: 84% fail initial PCI audit
Ongoing compliance costs: $50,000-$200,000 annually
Real-World Payment Handling Failures
Target Breach (2013): 41 million card accounts compromised because they stored payment data and had insufficient security. Settlement: $18.5 million
Home Depot Breach (2014): 56 million cards stolen. They were storing card data locally. Settlement: $17.5 million
The Secure Approach: Never Touch Card Data
By using Clerk Billing + Stripe, we never see, store, or transmit credit card data. We're not subject to PCI-DSS. Stripe is.
Our Payment Architecture
What Happens (What DOESN'T Happen)
User subscribes:
- Frontend shows Clerk's
PricingTablecomponent - User clicks subscribe → Clerk opens Stripe Checkout
- User enters card → Stripe's servers (not ours)
- Stripe processes payment → Stripe's servers (not ours)
- Stripe notifies Clerk → Webhook (verified by Clerk)
- Clerk updates subscription status
- Clerk notifies Convex → Webhook to our database
- Our app reads subscription status → Grants access
What Never Touches Our Servers
- ❌ Credit card numbers
- ❌ CVV codes
- ❌ Expiration dates
- ❌ Billing addresses (unless user separately provides)
What We Store
- ✅ Subscription status (free/basic/pro)
- ✅ Subscription start date
- ✅ Customer ID (Stripe's internal ID, not card info)
This Architecture Means
- We're NOT subject to PCI-DSS (Stripe is)
- We can't leak card data (we never have it)
- Stripe handles fraud detection
- Stripe handles 3D Secure
- Clerk handles webhook security
Implementation Files
components/custom-clerk-pricing.tsx- Pricing table componentapp/dashboard/payment-gated/page.tsx- Example of subscription gatingconvex/http.ts- Webhook receiver (signature verified by Svix)
Setting Up Clerk Billing
1. Configure in Clerk Dashboard
- Go to Clerk Dashboard → Billing
- Connect Stripe account
- Create subscription plans (Free, Basic, Pro)
- Copy Clerk Billing publishable key
2. Environment Variables
# .env.local
# Clerk Billing
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
# Stripe (automatically configured by Clerk Billing)
# No manual Stripe keys needed!
# Webhook signing secret (from Clerk)
CLERK_WEBHOOK_SECRET=whsec_...
3. Add Pricing Table Component
// components/custom-clerk-pricing.tsx
'use client';
import { PricingTable } from '@clerk/clerk-react';
export function CustomClerkPricing() {
return (
<div className="pricing-container">
<h1>Choose Your Plan</h1>
<PricingTable
appearance={{
elements: {
card: 'border rounded-lg p-6',
cardActive: 'border-blue-500',
button: 'bg-blue-600 hover:bg-blue-700 text-white',
}
}}
/>
</div>
);
}
Checking Subscription Status
Server-Side (API Routes)
// app/api/premium-feature/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import { handleUnauthorizedError, handleForbiddenError } from '@/lib/errorHandler';
export async function GET(request: NextRequest) {
const { userId, sessionClaims } = await auth();
if (!userId) {
return handleUnauthorizedError();
}
// Check subscription status from Clerk
const plan = sessionClaims?.metadata?.plan as string;
if (plan === 'free_user') {
return handleForbiddenError('Premium subscription required');
}
// User has paid subscription
return NextResponse.json({
message: 'Welcome to premium feature!',
plan: plan
});
}
Client-Side (Components)
'use client';
import { Protect } from '@clerk/nextjs';
import Link from 'next/link';
export function PremiumFeature() {
return (
<Protect
condition={(has) => !has({ plan: "free_user" })}
fallback={<UpgradePrompt />}
>
<div>
{/* Premium feature content */}
<h2>Premium Feature</h2>
<p>This content is only visible to paid subscribers</p>
</div>
</Protect>
);
}
function UpgradePrompt() {
return (
<div className="upgrade-prompt">
<h3>Upgrade to Premium</h3>
<p>This feature is available on our paid plans</p>
<Link href="/pricing">
<button>View Pricing</button>
</Link>
</div>
);
}
Complete Payment-Gated Page Example
// app/dashboard/payment-gated/page.tsx
'use client';
import { Protect } from '@clerk/nextjs';
import { CustomClerkPricing } from '@/components/custom-clerk-pricing';
export default function PaymentGatedPage() {
return (
<div>
<Protect
condition={(has) => !has({ plan: "free_user" })}
fallback={
<div className="upgrade-required">
<h1>Premium Access Required</h1>
<p>Subscribe to access this page</p>
<CustomClerkPricing />
</div>
}
>
<div className="premium-content">
<h1>Premium Dashboard</h1>
<p>Welcome to the premium features!</p>
{/* Premium features here */}
</div>
</Protect>
</div>
);
}
Webhook Handling
Clerk Webhook (User & Subscription Events)
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';
import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;
if (!WEBHOOK_SECRET) {
throw new Error('Missing CLERK_WEBHOOK_SECRET');
}
// Get webhook headers
const headerPayload = headers();
const svix_id = headerPayload.get("svix-id");
const svix_timestamp = headerPayload.get("svix-timestamp");
const svix_signature = headerPayload.get("svix-signature");
if (!svix_id || !svix_timestamp || !svix_signature) {
return new Response('Missing svix headers', { status: 400 });
}
const payload = await request.json();
const body = JSON.stringify(payload);
// Verify webhook signature
const wh = new Webhook(WEBHOOK_SECRET);
let evt: any;
try {
evt = wh.verify(body, {
"svix-id": svix_id,
"svix-timestamp": svix_timestamp,
"svix-signature": svix_signature,
});
} catch (err) {
console.error('Webhook verification failed:', err);
return new Response('Invalid signature', { status: 400 });
}
const { id, type, data } = evt;
// Handle subscription events
switch (type) {
case 'subscription.created':
await handleSubscriptionCreated(data);
break;
case 'subscription.updated':
await handleSubscriptionUpdated(data);
break;
case 'subscription.deleted':
await handleSubscriptionDeleted(data);
break;
case 'user.created':
await handleUserCreated(data);
break;
case 'user.updated':
await handleUserUpdated(data);
break;
}
return new Response('', { status: 200 });
}
async function handleSubscriptionCreated(data: any) {
const { user_id, plan, stripe_customer_id } = data;
// Store subscription in database
await db.subscriptions.create({
userId: user_id,
plan: plan,
stripeCustomerId: stripe_customer_id,
status: 'active',
createdAt: Date.now()
});
// Update user metadata
await db.users.update(
{ clerkId: user_id },
{ plan: plan, updatedAt: Date.now() }
);
}
async function handleSubscriptionUpdated(data: any) {
const { user_id, plan, status } = data;
await db.subscriptions.update(
{ userId: user_id },
{
plan: plan,
status: status,
updatedAt: Date.now()
}
);
// Update user plan
await db.users.update(
{ clerkId: user_id },
{ plan: plan }
);
}
async function handleSubscriptionDeleted(data: any) {
const { user_id } = data;
await db.subscriptions.update(
{ userId: user_id },
{
status: 'cancelled',
cancelledAt: Date.now()
}
);
// Downgrade to free
await db.users.update(
{ clerkId: user_id },
{ plan: 'free_user' }
);
}
Convex Webhook (Alternative)
If using Convex, you can receive Clerk webhooks directly:
// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { Webhook } from "svix";
import { internal } from "./_generated/api";
const http = httpRouter();
http.route({
path: "/clerk-webhook",
method: "POST",
handler: httpAction(async (ctx, request) => {
const payload = await request.text();
const headers = request.headers;
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
let evt: any;
try {
evt = wh.verify(payload, {
"svix-id": headers.get("svix-id")!,
"svix-timestamp": headers.get("svix-timestamp")!,
"svix-signature": headers.get("svix-signature")!,
});
} catch (err) {
return new Response("Invalid signature", { status: 400 });
}
const { type, data } = evt;
switch (type) {
case "subscription.created":
await ctx.runMutation(internal.subscriptions.create, {
userId: data.user_id,
plan: data.plan,
stripeCustomerId: data.stripe_customer_id,
});
break;
case "subscription.updated":
await ctx.runMutation(internal.subscriptions.update, {
userId: data.user_id,
plan: data.plan,
status: data.status,
});
break;
case "subscription.deleted":
await ctx.runMutation(internal.subscriptions.cancel, {
userId: data.user_id,
});
break;
}
return new Response("", { status: 200 });
}),
});
export default http;
Testing Payments
Test Mode (Stripe Test Cards)
Always use Stripe test cards in development:
Success: 4242 4242 4242 4242
Decline: 4000 0000 0000 0002
3D Secure: 4000 0025 0000 3155
Insufficient funds: 4000 0000 0000 9995
CVV: Any 3 digits
Expiry: Any future date
ZIP: Any 5 digits
Testing Subscription Flow
- Go to
/pricing - Click "Subscribe" on a paid plan
- Clerk opens Stripe Checkout
- Enter test card:
4242 4242 4242 4242 - Complete checkout
- Verify subscription status updated
- Check premium features are accessible
Testing Webhook
# Use Stripe CLI to forward webhooks to local
stripe listen --forward-to localhost:3000/api/webhooks/clerk
# Trigger test subscription event
stripe trigger subscription.created
Handling Failed Payments
Failed Payment Flow
- Stripe attempts to charge card
- Payment fails (expired card, insufficient funds, etc.)
- Stripe notifies Clerk
- Clerk sends webhook:
subscription.payment_failed - We notify user via email
- Stripe retries (smart retry logic)
- If still fails after retries → subscription cancelled
Implementing Payment Failure Handling
// In webhook handler
case 'subscription.payment_failed':
await handlePaymentFailed(data);
break;
async function handlePaymentFailed(data: any) {
const { user_id, attempt_count } = data;
// Update subscription status
await db.subscriptions.update(
{ userId: user_id },
{
status: 'past_due',
lastPaymentFailed: Date.now(),
failedAttempts: attempt_count
}
);
// Send email to user
await sendEmail({
to: getUserEmail(user_id),
subject: 'Payment Failed',
template: 'payment-failed',
data: {
attemptCount: attempt_count,
retryDate: calculateRetryDate(attempt_count)
}
});
}
Security Best Practices
1. Always Verify Webhooks
❌ DON'T trust webhook data without verification:
// Bad - no signature verification
export async function POST(request: NextRequest) {
const data = await request.json();
// Process data directly - could be forged!
}
✅ DO verify webhook signatures:
// Good - signature verified by Svix
const wh = new Webhook(WEBHOOK_SECRET);
const evt = wh.verify(body, headers); // Throws if invalid
// Now safe to process
2. Never Store Payment Info
❌ DON'T store card data:
// Bad - PCI-DSS violation
await db.payments.create({
userId,
cardNumber: '4242424242424242', // ❌ NEVER DO THIS
cvv: '123', // ❌ NEVER DO THIS
expiry: '12/25' // ❌ NEVER DO THIS
});
✅ DO store Stripe IDs only:
// Good - no card data
await db.subscriptions.create({
userId,
stripeCustomerId: 'cus_123', // ✅ Stripe internal ID
stripeSubscriptionId: 'sub_456', // ✅ Stripe internal ID
plan: 'pro',
status: 'active'
});
3. Check Subscription Status on Server
❌ DON'T rely on client-side checks:
// Bad - can be bypassed
'use client';
const { user } = useUser();
if (user?.publicMetadata?.plan === 'pro') {
// Show premium feature - attacker can fake this
}
✅ DO verify on server:
// Good - secure
export async function GET(request: NextRequest) {
const { sessionClaims } = await auth();
const plan = sessionClaims?.metadata?.plan;
if (plan !== 'pro') {
return handleForbiddenError();
}
// Premium feature access
}
4. Implement Idempotency
Handle duplicate webhooks (Stripe may retry):
// Track processed webhook IDs
const processedWebhooks = new Set<string>();
export async function POST(request: NextRequest) {
const evt = await verifyWebhook(request);
const { id } = evt;
// Check if already processed
if (processedWebhooks.has(id)) {
return new Response('Already processed', { status: 200 });
}
// Process webhook
await handleWebhook(evt);
// Mark as processed
processedWebhooks.add(id);
return new Response('', { status: 200 });
}
What Clerk Billing Handles
✅ Stripe API integration - No manual Stripe code needed ✅ Customer creation/management - Automatic ✅ Subscription lifecycle - Create, update, cancel ✅ Webhook signature verification - Built-in via Svix ✅ User/subscription sync - Automatic metadata updates ✅ Idempotency - Handles duplicate webhooks ✅ Retry logic - Smart retry on failed webhooks
What This Architecture Prevents
✅ PCI-DSS compliance burden - Not subject to PCI-DSS ✅ Card data breaches - We never have card data ✅ Payment fraud - Stripe's fraud detection ✅ Webhook forgery - Svix signature verification ✅ Man-in-the-middle attacks - Stripe Checkout is HTTPS only ✅ Session hijacking - Clerk's secure session management
Common Mistakes to Avoid
❌ DON'T try to process cards yourself ❌ DON'T store any payment card information ❌ DON'T trust webhook data without verification ❌ DON'T rely on client-side subscription checks for access control ❌ DON'T forget to handle failed payments ❌ DON'T expose Stripe secret keys in client code
✅ DO use Clerk Billing + Stripe Checkout ✅ DO verify webhook signatures (Svix) ✅ DO check subscription status on server ✅ DO handle webhook events (created, updated, cancelled) ✅ DO test with Stripe test cards in development ✅ DO implement idempotency for webhooks
References
- Clerk Billing Documentation: https://clerk.com/docs/billing/overview
- Stripe Checkout: https://stripe.com/docs/payments/checkout
- PCI-DSS Standards: https://www.pcisecuritystandards.org/
- Stripe Testing: https://stripe.com/docs/testing
- Webhook Security: https://docs.svix.com/
Next Steps
- For subscription-based access control: Use
auth-securityskill with Protect component - For webhook endpoint security: Combine with
rate-limitingskill - For error handling in payment processing: Use
error-handlingskill - For testing: Use
security-testingskill
GitHub 仓库
相关推荐技能
content-collections
元Content Collections 是一个 TypeScript 优先的构建工具,可将本地 Markdown/MDX 文件转换为类型安全的数据集合。它专为构建博客、文档站和内容密集型 Vite+React 应用而设计,提供基于 Zod 的自动模式验证。该工具涵盖从 Vite 插件配置、MDX 编译到生产环境部署的完整工作流。
polymarket
元这个Claude Skill为开发者提供完整的Polymarket预测市场开发支持,涵盖API调用、交易执行和市场数据分析。关键特性包括实时WebSocket数据流,可监控实时交易、订单和市场动态。开发者可用它构建预测市场应用、实施交易策略并集成实时市场预测功能。
hybrid-cloud-networking
元这个Skill帮助开发者配置本地基础设施与云平台之间的安全高性能连接。它支持VPN和专用连接选项,适用于构建混合云架构、连接数据中心到云以及实现安全的跨地域网络。关键能力包括建立AWS、Azure、GCP的混合连接,满足合规要求并支持渐进式云迁移。
llamaindex
元LlamaIndex是一个专门构建RAG应用的开发框架,提供300多种数据连接器用于文档摄取、索引和查询。它具备向量索引、查询引擎和智能代理等核心功能,支持构建文档问答、知识检索和聊天机器人等数据密集型应用。开发者可用它快速搭建连接私有数据与LLM的RAG管道。
