LogoBoring Template

Payments (Stripe)

Implement payments and subscriptions in your application using Stripe.

This template uses Stripe for payments and subscriptions. For more information, visit the Stripe documentation.

Setup

Prerequisites

Stripe Account

💳 Active Stripe account with API access

Products & Prices

🏷️ Configured in Stripe Dashboard

Webhook Setup

🔄 Endpoint for payment events

Installation

Install the required packages:

Terminal
npm install stripe

Environment Variables

Add the following variables to your .env.local file:

.env.local
# Stripe Configuration
STRIPE_SECRET_KEY=your_stripe_secret_key
STRIPE_WEBHOOK_SECRET=your_webhook_secret
STRIPE_STARTER_PRICE_ID=price_id_for_starter_plan
STRIPE_PRO_PRICE_ID=price_id_for_pro_plan

Never commit your Stripe secret key or webhook secret to version control. Always use environment variables.

Implementation

Stripe Client Setup

Configure the Stripe client in lib/stripe.ts:

lib/stripe.ts
import Stripe from "stripe";
 
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? "", {
  apiVersion: "2024-06-20",
  typescript: true,
});

Payment Button Component

Reusable component for initiating Stripe checkout:

components/stripe-button.tsx
export function StripeButton({
  type,
  text = "Buy",
  variant = "default",
  from = "landing",
  size = "default",
  icon,
  className,
}: StripeButtonProps) {
  const [isLoading, setIsLoading] = useState<boolean>(false);
 
  const createBillingSession = async () => {
    try {
      setIsLoading(true);
      const data = await stripeRedirect({ type, from });
      window.location.href = data;
    } catch (error: any) {
      toast.error(error?.message || "An error occurred");
    } finally {
      setIsLoading(false);
    }
  };
 
  return (
    <Button
      variant={variant}
      className={cn("w-full", className)}
      onClick={createBillingSession}
      disabled={isLoading}
      size={size}
    >
      {text}
      {icon && <Icon className="ml-2 size-4 shrink-0" />}
    </Button>
  );
}

Stripe Redirect Handler

Server action to create Stripe checkout sessions:

app/actions/stripe-redirect.ts
export async function stripeRedirect({
  type,
  from = "landing",
}: stripeRedirectProps) {
  // Verify environment and user
  if (!process.env.NEXT_PUBLIC_APP_URL) {
    throw new Error("NEXT_PUBLIC_APP_URL is not defined");
  }
 
  try {
    const successUrl = absoluteUrl(
      `${createRoute("success").href}?from=${from}`
    );
    const cancelUrl = absoluteUrl(`${createRoute("cancel").href}?from=${from}`);
 
    const plan = PLANS.find((plan) => plan.type === type);
    if (!plan?.price.priceIds.production) {
      throw new Error("Plan not found or price not configured");
    }
 
    const stripeSession = await stripe.checkout.sessions.create({
      success_url: successUrl + "&session_id={CHECKOUT_SESSION_ID}",
      cancel_url: cancelUrl,
      payment_method_types: ["card"],
      mode: "payment",
      billing_address_collection: "auto",
      customer_email: from === "dashboard" ? user?.email : undefined,
      line_items: [
        {
          quantity: 1,
          price: plan.price.priceIds.production,
        },
      ],
      metadata: {
        userId: from === "dashboard" ? user!.id : null,
        type: type,
        planName: plan.name,
      },
    });
 
    return stripeSession.url || "";
  } catch (error: any) {
    throw new Error(error);
  }
}

Features

Multiple Plans

🔢 Support for different pricing tiers and subscription plans

Secure Checkout

🔒 PCI-compliant payment processing with Stripe Checkout

User Context

👤 Different flows for authenticated and anonymous users

Metadata Tracking

📊 Track plan types and user information in payments

Webhooks

Webhooks are essential for receiving payment events from Stripe and updating your application's state accordingly.

Webhook Handler

Create a webhook handler to process Stripe events:

app/api/webhooks/stripe/route.ts
import { headers } from "next/headers";
import { NextResponse } from "next/server";
import { Stripe } from "stripe";
import { stripe } from "@/lib/stripe";
 
export async function POST(req: Request) {
  const body = await req.text();
  const signature = headers().get("Stripe-Signature") as string;
 
  let event: Stripe.Event;
 
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (error: any) {
    return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 });
  }
 
  // Handle the event
  switch (event.type) {
    case "checkout.session.completed":
      const session = event.data.object as Stripe.Checkout.Session;
 
      // Extract metadata
      const userId = session.metadata?.userId;
      const planType = session.metadata?.type;
 
      // Update user subscription in database
      // ... your code to update user subscription
 
      break;
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }
 
  return NextResponse.json({ received: true });
}

Testing Webhooks Locally

Use the Stripe CLI to test webhooks during development:

Terminal
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
 
# Login to your Stripe account
stripe login
 
# Forward webhook events to your local server
stripe listen --forward-to http://localhost:3000/api/webhooks/stripe

Payment Flow

Payment Process

The payment flow integrates Stripe Checkout for secure, PCI-compliant payment processing.

  1. Initiate Payment: User clicks payment button, triggering stripeRedirect server action
  2. Create Session: Server creates Stripe checkout session with plan details
  3. Redirect to Checkout: User is redirected to Stripe's hosted checkout page
  4. Process Payment: Stripe handles payment processing and security
  5. Handle Result: User is redirected back with success or cancel status

Subscription Management

Checking Subscription Status

Check if a user has an active subscription:

lib/subscription.ts
export async function hasActiveSubscription(userId: string) {
  try {
    const [subscription] = await db
      .select({
        status: subscriptions.status,
        planId: subscriptions.planId,
        currentPeriodEnd: subscriptions.currentPeriodEnd,
      })
      .from(subscriptions)
      .where(eq(subscriptions.userId, userId))
      .orderBy(desc(subscriptions.createdAt))
      .limit(1);
 
    if (!subscription) {
      return false;
    }
 
    return (
      subscription.status === "active" &&
      new Date(subscription.currentPeriodEnd) > new Date()
    );
  } catch (error) {
    console.error("Error checking subscription status:", error);
    return false;
  }
}

Customer Portal

Allow users to manage their subscriptions through Stripe Customer Portal:

app/actions/create-portal-link.ts
export async function createPortalLink() {
  try {
    const { user } = await getCurrentUser();
    if (!user?.stripeCustomerId) {
      throw new Error("No customer ID found");
    }
 
    const portalSession = await stripe.billingPortal.sessions.create({
      customer: user.stripeCustomerId,
      return_url: absoluteUrl("/dashboard/billing"),
    });
 
    return portalSession.url;
  } catch (error: any) {
    throw new Error(error.message);
  }
}

Best Practices

Security

  • Never log sensitive payment data
  • Use webhook signatures for verification
  • Implement proper error handling
  • Validate payment status server-side

User Experience

  • Show clear pricing information
  • Handle loading states properly
  • Provide clear error messages
  • Implement proper success/cancel flows

Pricing Plans Configuration

Define your pricing plans in a configuration file:

lib/config.ts
export const PLANS = [
  {
    type: "starter",
    name: "Starter",
    description: "Perfect for personal projects",
    price: {
      amount: 4.99,
      priceIds: {
        test: "price_1MKSNHLkdIwHu7ixo7ItY8QY",
        production: process.env.STRIPE_STARTER_PRICE_ID,
      },
    },
    features: [
      "Up to 3 projects",
      "Basic analytics",
      "48-hour support",
      "2 team members",
    ],
  },
  {
    type: "pro",
    name: "Pro",
    description: "For teams and professional projects",
    price: {
      amount: 9.99,
      priceIds: {
        test: "price_1MKSNCLkdIwHu7ix9zVZWq1p",
        production: process.env.STRIPE_PRO_PRICE_ID,
      },
    },
    features: [
      "Unlimited projects",
      "Advanced analytics",
      "24-hour support",
      "Unlimited team members",
      "Custom domain",
      "API access",
    ],
  },
];

Additional Resources