agentsclimarketplace

Payments stripe

Skill param087/saas-starter-skills/skills/payments-stripe

Production-grade full-stack SaaS skills for AI coding agents — Next.js, Postgres/Drizzle, Auth, Stripe & Vercel patterns for Codex, Claude Code, Cursor & OpenCode.

Install
npx -y skills add param087/saas-starter-skills --skill payments-stripe

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when integrating Stripe — set up Checkout for subscriptions, verify webhooks with the signing secret, handle events idempotently, and keep your database in sync with Stripe as the source of truth for billing state.

SKILL.md

3.2 KB, as published. Nobody here has run it

Payments (Stripe)

Overview

Stripe is the billing source of truth; your database is a synced read-model. The flow: send users to Checkout, then let webhooks drive every state change (subscription created/updated/cancelled, payment succeeded/failed). Never set a user to "paid" from the success-redirect — that's spoofable. Verify webhook signatures, process events idempotently, and reconcile to your DB.

When to use

  • Charging for subscriptions or one-time purchases.
  • Reacting to payment/subscription lifecycle changes.
  • Building an upgrade/downgrade or customer-portal flow.

Checkout (start a subscription)

// server action / route handler
import Stripe from "stripe";
import { env } from "@/env";
const stripe = new Stripe(env.STRIPE_SECRET_KEY);

export async function createCheckout(orgId: string, priceId: string) {
  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${env.NEXT_PUBLIC_APP_URL}/billing?success=1`,
    cancel_url: `${env.NEXT_PUBLIC_APP_URL}/billing`,
    client_reference_id: orgId,            // tie the session back to YOUR tenant
    metadata: { orgId },
  });
  return session.url!;
}

Webhook (the source of truth)

// src/app/api/webhooks/stripe/route.ts
import Stripe from "stripe";
import { env } from "@/env";
const stripe = new Stripe(env.STRIPE_SECRET_KEY);

export async function POST(req: Request) {
  const body = await req.text();                       // RAW body, not parsed JSON
  const sig = req.headers.get("stripe-signature")!;
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, env.STRIPE_WEBHOOK_SECRET);
  } catch {
    return new Response("Invalid signature", { status: 400 });
  }

  if (await alreadyProcessed(event.id)) return new Response("ok"); // idempotency

  switch (event.type) {
    case "checkout.session.completed":
    case "customer.subscription.updated":
    case "customer.subscription.deleted":
      await syncSubscriptionToDb(event);               // upsert plan/status/period
      break;
  }
  await markProcessed(event.id);
  return new Response("ok");
}

Pitfalls

  • Granting access on the success redirect — users can hit that URL without paying. Only webhooks grant access.
  • Parsing the body before verifying — signature checks need the raw body; disable body parsing for this route.
  • No idempotency — Stripe retries; store processed event.ids so retries don't double-apply.
  • Returning 500 on a handled event — Stripe will retry forever. Return 2xx once you've safely recorded it.
  • Storing card data — never; Stripe holds it. You store customer/subscription IDs only.
  • Trusting metadata you didn't set — only read metadata you wrote (e.g. orgId).

Hand-off

Stripe events synced into your DB. subscription-billing turns that synced state into plan limits and entitlements; the customer portal handles upgrades/cancellations.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.