Claude
Skills
Sign in
Back

payments

Included with Lifetime
$97 forever

Credit-based billing with Stripe — subscriptions, credit balance, usage metering, checkout, billing portal. Use this skill when the user says "add payments", "setup billing", "add stripe", "setup credits", "subscription billing", or "stripe integration".

Sales & CRM

What this skill does


# Payments (Stripe)

Credit-based billing using [Stripe](https://stripe.com). Subscription plans (free/pro/team) with monthly credit allocations, one-time credit purchases, usage metering, and full webhook handling. Local development with Stripe CLI via Docker.

## Prerequisites

- Next.js app with App Router (no `src/` directory)
- `auth` skill applied (for user authentication)
- `db` skill applied (Drizzle + Postgres)
- `env-config` skill applied
- `docker` skill applied (for Stripe CLI)

## Installation

```bash
bun add stripe @stripe/stripe-js
```

## Environment Variables

Add to `.env.local`:

```env
# Stripe
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
```

Add to `env.ts`:

```typescript
// Server schema
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_"),

// Client schema
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_"),
```

## Docker Setup

Add Stripe CLI to `docker-compose.yml`:

```yaml
services:
  stripe-cli:
    image: stripe/stripe-cli:latest
    command: listen --forward-to http://host.docker.internal:3000/api/stripe/webhook --api-key ${STRIPE_SECRET_KEY}
    environment:
      - STRIPE_API_KEY=${STRIPE_SECRET_KEY}
    extra_hosts:
      - "host.docker.internal:host-gateway"
    profiles:
      - dev
```

Start with:

```bash
docker compose --profile dev up stripe-cli
```

## What Gets Created

```
app/
├── api/
│   ├── stripe/
│   │   └── webhook/
│   │       └── route.ts            # Stripe webhook handler
│   └── billing/
│       ├── route.ts                # GET subscription + balance
│       ├── checkout/
│       │   └── route.ts            # POST create checkout session
│       └── portal/
│           └── route.ts            # POST create billing portal
lib/
└── payments/
    ├── stripe.ts                   # Stripe client setup
    ├── plans.ts                    # Plan definitions and credit costs
    ├── credits.ts                  # Credit balance helpers
    └── client.ts                   # Client-side helpers
db/
└── schema/
    ├── subscriptions.ts            # Subscriptions table
    └── credit-transactions.ts      # Credit ledger table
components/
└── billing/
    ├── plan-selector.tsx           # Plan comparison cards
    ├── credit-balance.tsx          # Credit balance badge
    └── upgrade-dialog.tsx          # Upgrade prompt dialog
```

## Setup Steps

### Step 1: Create `db/schema/subscriptions.ts`

```typescript
import { pgTable, text, integer, timestamp, boolean, uuid } from "drizzle-orm/pg-core";

export const subscriptions = pgTable("subscriptions", {
  id: uuid("id").primaryKey().defaultRandom(),
  userId: text("user_id").notNull().unique(),
  stripeCustomerId: text("stripe_customer_id").notNull(),
  stripeSubscriptionId: text("stripe_subscription_id"),
  stripePriceId: text("stripe_price_id"),
  plan: text("plan", {
    enum: ["free", "pro", "team"],
  }).notNull().default("free"),
  status: text("status", {
    enum: ["active", "past_due", "canceled", "trialing"],
  }).notNull().default("active"),
  creditBalance: integer("credit_balance").notNull().default(0),
  currentPeriodStart: timestamp("current_period_start"),
  currentPeriodEnd: timestamp("current_period_end"),
  cancelAtPeriodEnd: boolean("cancel_at_period_end").default(false),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

export type Subscription = typeof subscriptions.$inferSelect;
export type NewSubscription = typeof subscriptions.$inferInsert;
```

### Step 2: Create `db/schema/credit-transactions.ts`

```typescript
import { pgTable, text, integer, timestamp, uuid } from "drizzle-orm/pg-core";

export const creditTransactions = pgTable("credit_transactions", {
  id: uuid("id").primaryKey().defaultRandom(),
  userId: text("user_id").notNull(),
  amount: integer("amount").notNull(),
  type: text("type", {
    enum: ["credit", "debit"],
  }).notNull(),
  source: text("source", {
    enum: ["subscription", "purchase", "usage", "bonus", "refund"],
  }).notNull(),
  sourceId: text("source_id"),
  description: text("description"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

export type CreditTransaction = typeof creditTransactions.$inferSelect;
export type NewCreditTransaction = typeof creditTransactions.$inferInsert;
```

### Step 3: Add exports to `db/schema/index.ts`

```typescript
export * from "./subscriptions";
export * from "./credit-transactions";
```

### Step 4: Create `lib/payments/stripe.ts`

```typescript
import Stripe from "stripe";

const stripeKey = process.env.STRIPE_SECRET_KEY;
if (!stripeKey) throw new Error("STRIPE_SECRET_KEY is required");

export const stripe = new Stripe(stripeKey, {
  apiVersion: "2026-01-28.clover",
  typescript: true,
});

export async function getOrCreateCustomer(params: {
  userId: string;
  email: string;
  name?: string;
}): Promise<Stripe.Customer> {
  const existing = await stripe.customers.list({
    email: params.email,
    limit: 1,
  });

  if (existing.data.length > 0) {
    return existing.data[0];
  }

  return stripe.customers.create({
    email: params.email,
    name: params.name,
    metadata: { userId: params.userId },
  });
}
```

### Step 5: Create `lib/payments/plans.ts`

```typescript
export type PlanName = "free" | "pro" | "team";

export type PlanConfig = {
  name: string;
  description: string;
  monthlyCredits: number;
  priceMonthly: number;
  features: string[];
};

export const PLANS: Record<PlanName, PlanConfig> = {
  free: {
    name: "Free",
    description: "Get started with basic features",
    monthlyCredits: 50,
    priceMonthly: 0,
    features: [
      "50 credits/month",
      "Basic image generation",
      "Community support",
    ],
  },
  pro: {
    name: "Pro",
    description: "For professionals and creators",
    monthlyCredits: 500,
    priceMonthly: 29,
    features: [
      "500 credits/month",
      "All models (Flux Pro, SDXL, etc.)",
      "Video generation",
      "Priority processing",
      "Email support",
    ],
  },
  team: {
    name: "Team",
    description: "For teams and businesses",
    monthlyCredits: 2000,
    priceMonthly: 99,
    features: [
      "2,000 shared credits/month",
      "All Pro features",
      "Real-time collaboration",
      "Team admin dashboard",
      "Priority support",
    ],
  },
};

export type OperationType =
  | "image-generation"
  | "image-generation-pro"
  | "video-generation"
  | "image-to-image"
  | "inpainting";

export const CREDIT_COSTS: Record<OperationType, number> = {
  "image-generation": 1,
  "image-generation-pro": 5,
  "video-generation": 20,
  "image-to-image": 2,
  "inpainting": 3,
};
```

### Step 6: Create `lib/payments/credits.ts`

```typescript
import { db } from "@/db";
import { subscriptions, creditTransactions } from "@/db/schema";
import { eq, sql } from "drizzle-orm";
import type { OperationType } from "./plans";
import { CREDIT_COSTS } from "./plans";

export async function getBalance(userId: string): Promise<number> {
  const [sub] = await db
    .select({ creditBalance: subscriptions.creditBalance })
    .from(subscriptions)
    .where(eq(subscriptions.userId, userId))
    .limit(1);

  return sub?.creditBalance ?? 0;
}

export async function hasCredits(
  userId: string,
  operation: OperationType,
  quantity = 1
): Promise<boolean> {
  const balance = await getBalance(userId);
  return balance >= CREDIT_COSTS[operation] * quantity;
}

export async function deductCredits(params: {
  userId: string;
  operation: OperationType;
  quantity?: number;
  description?: string;
}): Promise<{ success: boolean; cost: number; newBalance: number; error?: string }> {
  const { userId, operation, quantity = 1, description } = params;
  const cost = CREDIT_COSTS[operation] * quantity;
  const balance = await getBalance(userId);

  if (balance < cost) {
    return {
   
Files: 1
Size: 27.0 KB
Complexity: 36/100
Category: Sales & CRM

Related in Sales & CRM