Skill

stripe-setup

Install
1
Install the plugin
$
npx claudepluginhub mickaelmamani/saas-toolkit --plugin saas-toolkit

Want just this skill?

Add to a custom plugin, then install with one command.

Description

Stripe payments and subscriptions for SaaS. Use when implementing checkout, billing portal, webhooks, or subscription management.

Tool Access

This skill is limited to using the following tools:

ReadWriteEditGrepGlobBashWebSearchWebFetchmcp__supabasemcp__stripe
Skill Content

/stripe-setup — Stripe SaaS Integration

Complete Stripe integration for a SaaS application with Next.js and Supabase.

Setup

1. Install & configure

npm install stripe @supabase/stripe-sync-engine

Environment variables:

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...

Stripe client (lib/stripe.ts):

import Stripe from 'stripe';

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2025-12-18.acacia',
  typescript: true,
});

2. Product & pricing model

Define products and prices in Stripe Dashboard or via API:

// Typical SaaS pricing structure
// Product: "Pro Plan"
//   - Price: $19/month (price_xxx)
//   - Price: $190/year (price_yyy)
// Product: "Enterprise Plan"
//   - Price: $49/month (price_zzz)

Store price IDs in environment variables or a config file — never hardcode.

3. Stripe Sync Engine (recommended)

Use @supabase/stripe-sync-engine to auto-sync all Stripe data into a stripe schema in your Supabase database. This replaces manual subscription tables and most webhook handling.

Run migrations (one-time setup):

import { runMigrations } from '@supabase/stripe-sync-engine';

await runMigrations({
  databaseUrl: process.env.DATABASE_URL!,
  schema: 'stripe',
});

Deploy as Supabase Edge Function (supabase/functions/stripe-sync/index.ts):

import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
import { StripeSync } from 'npm:@supabase/stripe-sync-engine'

const stripeSync = new StripeSync({
  poolConfig: {
    connectionString: Deno.env.get('DATABASE_URL')!,
    max: 20,
    keepAlive: true,
  },
  stripeWebhookSecret: Deno.env.get('STRIPE_WEBHOOK_SECRET')!,
  stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,
  backfillRelatedEntities: false,
  autoExpandLists: true,
})

Deno.serve(async (req) => {
  const rawBody = new Uint8Array(await req.arrayBuffer())
  const stripeSignature = req.headers.get('stripe-signature')
  await stripeSync.processWebhook(rawBody, stripeSignature)
  return new Response(null, { status: 202 })
})

Grant access to the stripe schema for your app's database role:

GRANT USAGE ON SCHEMA stripe TO authenticated;
GRANT SELECT ON ALL TABLES IN SCHEMA stripe TO authenticated;

This creates tables for: customers, subscriptions, products, prices, invoices, payment_intents, charges, and more — all synced automatically via Stripe webhooks.

4. Linking Stripe customers to your users

You still need a mapping between auth.users and Stripe customers. Add stripe_customer_id to your profiles table:

ALTER TABLE public.profiles ADD COLUMN stripe_customer_id text;
CREATE INDEX idx_profiles_stripe_customer_id ON public.profiles(stripe_customer_id);

Query subscription status by joining your profiles with the sync engine's stripe.subscriptions table:

SELECT s.status, s.current_period_end
FROM stripe.subscriptions s
JOIN stripe.customers c ON s.customer = c.id
JOIN public.profiles p ON p.stripe_customer_id = c.id
WHERE p.id = auth.uid();

4. Checkout Session (Server Action)

'use server';
import { stripe } from '@/lib/stripe';
import { createClient } from '@/lib/supabase/server';
import { redirect } from 'next/navigation';

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
  let { data: profile } = await supabase
    .from('profiles')
    .select('stripe_customer_id')
    .eq('id', user.id)
    .single();

  let customerId = profile?.stripe_customer_id;
  if (!customerId) {
    const customer = await stripe.customers.create({
      email: user.email,
      metadata: { user_id: user.id },
    });
    customerId = customer.id;
  }

  const session = await stripe.checkout.sessions.create({
    customer: customerId,
    line_items: [{ price: priceId, quantity: 1 }],
    mode: 'subscription',
    success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/dashboard?success=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing?canceled=true`,
    metadata: { user_id: user.id },
  });

  if (session.url) redirect(session.url);
}

5. Webhook handling

With Stripe Sync Engine (recommended): All Stripe data sync is handled by the Edge Function deployed in step 3. The sync engine processes 80+ webhook event types automatically.

For custom business logic (e.g., sending emails, provisioning access after checkout), add a separate Next.js webhook route that handles only your app-specific events:

app/api/webhooks/stripe/route.ts:

import { stripe } from '@/lib/stripe';
import { createClient } from '@supabase/supabase-js';
import { NextResponse } from 'next/server';

// Use service role for webhook — no user context
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

export async function POST(request: Request) {
  const body = await request.text(); // MUST be raw text
  const signature = request.headers.get('stripe-signature')!;

  let event;
  try {
    event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch (err) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
  }

  // Only handle app-specific business logic here
  // Data sync (subscriptions, invoices, etc.) is handled by stripe-sync-engine
  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckoutComplete(event.data.object);
      break;
    case 'invoice.payment_failed':
      await handlePaymentFailed(event.data.object);
      break;
  }

  return NextResponse.json({ received: true });
}

Tip: Configure two Stripe webhook endpoints — one pointing to the Edge Function (all events), one pointing to your Next.js route (only app-specific events).

6. Customer Portal

'use server';
import { stripe } from '@/lib/stripe';
import { createClient } from '@/lib/supabase/server';
import { redirect } from 'next/navigation';

export async function createPortalSession() {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) redirect('/login');

  const { data: profile } = await supabase
    .from('profiles')
    .select('stripe_customer_id')
    .eq('id', user.id)
    .single();

  if (!profile?.stripe_customer_id) redirect('/pricing');

  const session = await stripe.billingPortal.sessions.create({
    customer: profile.stripe_customer_id,
    return_url: `${process.env.NEXT_PUBLIC_SITE_URL}/dashboard/billing`,
  });

  redirect(session.url);
}

7. Subscription gating

Query the stripe.subscriptions table (populated by the sync engine) joined with your profiles:

// lib/subscription.ts
import { createClient } from '@/lib/supabase/server';

export async function getSubscription() {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return null;

  // Get the user's stripe_customer_id from profiles
  const { data: profile } = await supabase
    .from('profiles')
    .select('stripe_customer_id')
    .eq('id', user.id)
    .single();

  if (!profile?.stripe_customer_id) return null;

  // Query the stripe schema (synced by stripe-sync-engine)
  const { data } = await supabase
    .schema('stripe')
    .from('subscriptions')
    .select('id, status, current_period_end, items:subscription_items(price:prices(*))')
    .eq('customer', profile.stripe_customer_id)
    .in('status', ['active', 'trialing'])
    .single();

  return data;
}

export async function requireSubscription() {
  const subscription = await getSubscription();
  if (!subscription) redirect('/pricing');
  return subscription;
}

8. Testing with Stripe CLI

# Listen for webhooks locally
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# Trigger test events
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_failed

Rules

  • NEVER use Charges API, Card Element, or Sources — use Checkout Sessions
  • ALWAYS verify webhook signatures with constructEvent()
  • ALWAYS use request.text() for webhook body — never request.json()
  • Use @supabase/stripe-sync-engine for Stripe data sync — don't build custom subscription tables
  • Query subscription status from the stripe schema (not the Stripe API)
  • Use Server Actions for creating Checkout Sessions and Portal Sessions
  • Use service_role key in webhook handler (no user context available)
  • Keep Stripe secret key server-side only — never expose to client
  • Deploy the sync engine as a Supabase Edge Function for webhook processing
  • Use a separate Next.js webhook route only for custom business logic (emails, provisioning)
Stats
Stars0
Forks0
Last CommitFeb 11, 2026
Actions

Similar Skills

cache-components

Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). **PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations. **DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions. **USE CASES**: Implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, debugging cache issues, and reviewing Cache Component implementations.

138.4k