Stripe Integration

Learn how to implement payments and subscriptions using Stripe in your application.

Setup

The boilerplate comes with pre-configured Stripe integration. Follow these steps to set it up:

1. Environment Variables

Add your Stripe API keys to .env.local:

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

2. Stripe CLI Setup

Install and configure the Stripe CLI for local webhook testing:

# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login to Stripe
stripe login

# Forward webhooks to your local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe

Implementing Payments

One-time Payments

Implement one-time payments using Stripe Checkout:

// app/api/checkout/route.ts
import { stripe } from "@/lib/stripe";
import { createClient } from "@/lib/supabase/server";

export async function POST(request: Request) {
  try {
    const supabase = createClient();
    
    // Get authenticated user
    const { data: { user }, error: authError } = 
      await supabase.auth.getUser();
    if (authError || !user) {
      return new Response("Unauthorized", { status: 401 });
    }

    // Create Stripe checkout session
    const session = await stripe.checkout.sessions.create({
      customer_email: user.email,
      line_items: [
        {
          price: "price_H5ggYwtDq4fbrJ",
          quantity: 1,
        },
      ],
      mode: "payment",
      success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/success`,
      cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/cancel`,
    });

    return Response.json({ url: session.url });
  } catch (error) {
    console.error("Stripe error:", error);
    return new Response("Error creating checkout session", { 
      status: 500 
    });
  }
}

Subscription Payments

Implement recurring subscriptions:

// app/api/subscription/route.ts
import { stripe } from "@/lib/stripe";
import { createClient } from "@/lib/supabase/server";

export async function POST(request: Request) {
  try {
    const supabase = createClient();
    
    // Get authenticated user
    const { data: { user }, error: authError } = 
      await supabase.auth.getUser();
    if (authError || !user) {
      return new Response("Unauthorized", { status: 401 });
    }

    // Create Stripe subscription checkout
    const session = await stripe.checkout.sessions.create({
      customer_email: user.email,
      line_items: [
        {
          price: "price_H5ggYwtDq4fbrJ", // Your subscription price ID
          quantity: 1,
        },
      ],
      mode: "subscription",
      success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/dashboard`,
      cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing`,
      subscription_data: {
        metadata: {
          userId: user.id,
        },
      },
    });

    return Response.json({ url: session.url });
  } catch (error) {
    console.error("Stripe error:", error);
    return new Response("Error creating subscription", { 
      status: 500 
    });
  }
}

Webhook Handling

Handle Stripe webhooks to update your database when events occur:

// app/api/webhooks/stripe/route.ts
import { headers } from "next/headers";
import { stripe } from "@/lib/stripe";
import { createClient } from "@/lib/supabase/server";

export async function POST(request: Request) {
  const body = await request.text();
  const signature = headers().get("Stripe-Signature") as string;

  let event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (error) {
    console.error("Webhook signature verification failed");
    return new Response("Webhook signature verification failed", { 
      status: 400 
    });
  }

  const supabase = createClient();

  try {
    switch (event.type) {
      case "customer.subscription.created":
        const subscription = event.data.object;
        await supabase
          .from("subscriptions")
          .insert({
            id: subscription.id,
            user_id: subscription.metadata.userId,
            status: subscription.status,
            price_id: subscription.items.data[0].price.id,
            quantity: subscription.items.data[0].quantity,
            cancel_at_period_end: subscription.cancel_at_period_end,
            cancel_at: subscription.cancel_at
              ? new Date(subscription.cancel_at * 1000)
              : null,
            canceled_at: subscription.canceled_at
              ? new Date(subscription.canceled_at * 1000)
              : null,
            current_period_start: new Date(
              subscription.current_period_start * 1000
            ),
            current_period_end: new Date(
              subscription.current_period_end * 1000
            ),
            created: new Date(subscription.created * 1000),
            ended_at: subscription.ended_at
              ? new Date(subscription.ended_at * 1000)
              : null,
            trial_start: subscription.trial_start
              ? new Date(subscription.trial_start * 1000)
              : null,
            trial_end: subscription.trial_end
              ? new Date(subscription.trial_end * 1000)
              : null,
          });
        break;
      
      // Handle other webhook events...
    }

    return new Response(null, { status: 200 });
  } catch (error) {
    console.error("Error handling webhook:", error);
    return new Response("Webhook handler failed", { status: 500 });
  }
}

Client Implementation

Implement the payment UI in your React components:

// components/checkout-button.tsx
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";

export function CheckoutButton() {
  const [loading, setLoading] = useState(false);

  const handleCheckout = async () => {
    try {
      setLoading(true);
      
      const response = await fetch("/api/checkout", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
      });
      
      const data = await response.json();
      
      // Redirect to Stripe Checkout
      window.location.href = data.url;
    } catch (error) {
      console.error("Error:", error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <Button 
      onClick={handleCheckout}
      disabled={loading}
    >
      {loading ? "Loading..." : "Buy Now"}
    </Button>
  );
}

Next Steps