.claude/skills/stripe-connect-marketplace/SKILL.md
Master Stripe Connect for marketplace payments in RidenDine. Use when: (1) onboarding chefs to Stripe Connect, (2) implementing payment flows with platform fees, (3) handling webhooks, (4) managing payouts, (5) debugging payment issues. Key insight: RidenDine uses Standard Connect accounts with 15% platform fee deducted automatically via application_fee_amount.
npx skillsauth add Ritenoob/ridedine stripe-connect-marketplaceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
RidenDine is a marketplace connecting customers with chefs. Stripe Connect enables:
Use this skill when:
Account Types:
Payment Flow:
Customer → Stripe Checkout → Chef's Connected Account (85%) + Platform Fee (15%)
Key Components:
create_connect_account - Onboards chefscreate_checkout_session - Creates payment sessionswebhook_stripe - Handles payment eventschefs.stripe_account_id stores connected account IDsLocation: backend/supabase/functions/create_connect_account/index.ts
Flow:
profile_idstripe_account_idExample Implementation:
import Stripe from 'https://esm.sh/[email protected]';
import { createClient } from 'https://esm.sh/@supabase/[email protected]';
Deno.serve(async (req) => {
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, {
apiVersion: '2023-10-16',
});
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
const { profile_id, email, refresh_url, return_url } = await req.json();
// Create Standard Connect account
const account = await stripe.accounts.create({
type: 'standard',
email,
metadata: { profile_id },
});
// Create Account Link for onboarding
const accountLink = await stripe.accountLinks.create({
account: account.id,
refresh_url: refresh_url || 'https://ridendine.com/chef/onboarding',
return_url: return_url || 'https://ridendine.com/chef/dashboard',
type: 'account_onboarding',
});
// Update database with stripe_account_id
const { error } = await supabase
.from('chefs')
.update({ stripe_account_id: account.id })
.eq('profile_id', profile_id);
if (error) throw error;
return new Response(
JSON.stringify({ url: accountLink.url }),
{ headers: { 'Content-Type': 'application/json' } }
);
});
Database Update:
-- chefs table already has stripe_account_id column
-- Migration: backend/supabase/migrations/20240101000000_init.sql
ALTER TABLE chefs ADD COLUMN stripe_account_id TEXT UNIQUE;
Frontend Integration (Next.js):
// apps/web/app/chef/onboarding/page.tsx
async function connectStripe() {
const response = await fetch('/api/connect-account', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
profile_id: user.id,
email: user.email,
refresh_url: window.location.href,
return_url: `${window.location.origin}/chef/dashboard`,
}),
});
const { url } = await response.json();
window.location.href = url; // Redirect to Stripe onboarding
}
Location: backend/supabase/functions/create_checkout_session/index.ts
Flow:
stripe_account_idapplication_fee_amountExample Implementation:
import Stripe from 'https://esm.sh/[email protected]';
import { createClient } from 'https://esm.sh/@supabase/[email protected]';
Deno.serve(async (req) => {
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, {
apiVersion: '2023-10-16',
});
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
const { order_id } = await req.json();
// Fetch order details from database
const { data: order, error: orderError } = await supabase
.from('orders')
.select('*, chefs!inner(stripe_account_id), order_items(*)')
.eq('id', order_id)
.single();
if (orderError) throw orderError;
if (!order.chefs.stripe_account_id) {
throw new Error('Chef has not completed Stripe onboarding');
}
// Calculate total (server-side validation)
const totalCents = order.order_items.reduce(
(sum: number, item: any) => sum + item.price_cents * item.quantity,
0
);
// Platform fee: 15% of total
const platformFeeCents = Math.floor(totalCents * 0.15);
// Create Stripe Checkout Session
const session = await stripe.checkout.sessions.create(
{
mode: 'payment',
line_items: order.order_items.map((item: any) => ({
price_data: {
currency: 'usd',
product_data: { name: item.dish_name || 'Dish' },
unit_amount: item.price_cents,
},
quantity: item.quantity,
})),
payment_intent_data: {
application_fee_amount: platformFeeCents,
metadata: { order_id: order.id },
},
success_url: `${req.headers.get('origin')}/order/${order.id}?success=true`,
cancel_url: `${req.headers.get('origin')}/checkout?canceled=true`,
metadata: { order_id: order.id },
},
{
stripeAccount: order.chefs.stripe_account_id, // Payment goes to chef's account
}
);
return new Response(
JSON.stringify({ url: session.url }),
{ headers: { 'Content-Type': 'application/json' } }
);
});
Frontend Integration:
// apps/web/app/checkout/page.tsx
async function handleCheckout() {
// Create draft order in database first
const { data: order } = await supabase
.from('orders')
.insert({
customer_id: user.id,
chef_id: cart.chefId,
total_cents: cart.total,
status: 'draft',
})
.select()
.single();
// Create order items
await supabase.from('order_items').insert(
cart.items.map((item) => ({
order_id: order.id,
dish_id: item.id,
quantity: item.quantity,
price_cents: item.price * 100,
}))
);
// Create Stripe Checkout Session
const response = await fetch('/api/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_id: order.id }),
});
const { url } = await response.json();
window.location.href = url; // Redirect to Stripe Checkout
}
Location: backend/supabase/functions/webhook_stripe/index.ts
Critical: Stripe webhook signature verification prevents unauthorized events.
Events to Handle:
checkout.session.completed → Update order status to "placed"payment_intent.succeeded → Confirm payment successaccount.updated → Chef completed onboarding or updated detailspayout.paid → Chef received payoutExample Implementation:
import Stripe from 'https://esm.sh/[email protected]';
import { createClient } from 'https://esm.sh/@supabase/[email protected]';
Deno.serve(async (req) => {
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, {
apiVersion: '2023-10-16',
});
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
const signature = req.headers.get('stripe-signature');
const webhookSecret = Deno.env.get('STRIPE_WEBHOOK_SECRET')!;
if (!signature || !webhookSecret) {
return new Response('Missing signature or secret', { status: 400 });
}
const body = await req.text();
// ⚠️ CRITICAL: Verify webhook signature
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err: any) {
console.error('Webhook signature verification failed:', err.message);
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
// Handle different event types
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
const orderId = session.metadata?.order_id;
if (orderId) {
// Update order status to "placed" (payment confirmed)
await supabase
.from('orders')
.update({ status: 'placed', payment_status: 'paid' })
.eq('id', orderId);
}
break;
}
case 'payment_intent.succeeded': {
const paymentIntent = event.data.object as Stripe.PaymentIntent;
const orderId = paymentIntent.metadata?.order_id;
if (orderId) {
await supabase
.from('orders')
.update({ payment_status: 'paid' })
.eq('id', orderId);
}
break;
}
case 'account.updated': {
const account = event.data.object as Stripe.Account;
const profileId = account.metadata?.profile_id;
if (profileId && account.charges_enabled) {
// Chef completed onboarding and can accept payments
await supabase
.from('chefs')
.update({
stripe_account_id: account.id,
stripe_onboarding_complete: true,
})
.eq('profile_id', profileId);
}
break;
}
case 'payout.paid': {
const payout = event.data.object as Stripe.Payout;
// Log payout for chef's records (optional)
console.log(`Payout ${payout.id} paid to account ${payout.destination}`);
break;
}
default:
console.log(`Unhandled event type: ${event.type}`);
}
return new Response(JSON.stringify({ received: true }), {
headers: { 'Content-Type': 'application/json' },
});
});
Webhook Configuration:
Development: Use Stripe CLI to forward webhooks
stripe listen --forward-to http://localhost:54321/functions/v1/webhook_stripe
Production: Configure webhook endpoint in Stripe Dashboard
https://<project-id>.supabase.co/functions/v1/webhook_stripecheckout.session.completed, payment_intent.succeeded, account.updated, payout.paidSTRIPE_WEBHOOK_SECRETTest Cards:
4242 4242 4242 42424000 0000 0000 00024000 0027 6000 3184Test Environment Variables:
# .env.local (Development)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_... # From Stripe CLI or Dashboard
Supabase Edge Function Secrets:
# Set via Supabase Dashboard → Settings → Secrets
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
Testing Checklist:
chefs.stripe_account_id populated after onboardingcheckout.session.completed eventSymptom: Checkout fails with error about missing stripe_account_id
Cause: Chef's connected account not created or onboarding incomplete
Fix:
SELECT stripe_account_id FROM chefs WHERE id = '<chef-id>'create_connect_account Edge FunctionSymptom: Order status stays "draft" after payment
Cause: Webhook endpoint not configured or signature verification failing
Fix:
stripe listen --forward-to ...supabase functions logs webhook_stripeSTRIPE_WEBHOOK_SECRET matches Stripe Dashboardstripe trigger checkout.session.completed
Symptom: Chef receives full payment amount (100%), not 85%
Cause: application_fee_amount not set correctly
Fix:
create_checkout_session/index.tsplatformFeeCents = Math.floor(totalCents * 0.15)payment_intent_data.application_fee_amount is passedSymptom: Stripe API returns 404 for connected account
Cause: stripe_account_id in database doesn't exist in Stripe
Fix:
stripe_account_id from databasecreate_connect_accountstripe_account_id from Stripe Dashboardstripe.webhooks.constructEvent()stripe_account_id exists before checkoutbackend/supabase/functions/development
Integrate Coinbase crypto payments into payment systems. Use when: (1) adding crypto payment support, (2) building onchain features, (3) implementing wallet functionality. Covers Coinbase Commerce (payment processor) vs CDP (developer platform), Server Wallets, Embedded Wallets, and multi-network support.
development
Add Apple Pay and Google Pay to Stripe checkout. Use when: (1) adding mobile wallet payments, (2) improving mobile conversion, (3) implementing one-tap checkout. Stripe Payment Request Button automatically detects device capabilities and shows Apple Pay (Safari/iOS) or Google Pay (Chrome/Android).
development
Master Vercel deployment for RidenDine web and admin Next.js apps. Use when: (1) deploying to production, (2) configuring environment variables, (3) setting up preview deployments, (4) debugging build failures, (5) configuring domains, (6) seeing "No Next.js version detected" error in Vercel builds, (7) setting up monorepo with separate projects on free tier. Key insight: Vercel monorepos require Root Directory configuration via dashboard (not vercel.json), GitHub integration auto-detects monorepo structure, free tier allows multiple projects.
development
Master Supabase Row Level Security (RLS) for RidenDine. Use when: (1) adding new tables, (2) modifying RLS policies, (3) debugging access control issues, (4) role-based data access. Key insight: All tables use RLS with role-based policies from profiles.role column.