Stripe Setup in Nextbase Starter Kit.

This guide will walk you through how Stripe has been set up in different files in Nextbase Starter Kit..

Stripe Configuration

Stripe configuration is stored in the .env file. You need to provide your Stripe API keys here.

.env.local
STRIPE_PUBLIC_KEY=your_stripe_public_key
STRIPE_SECRET_KEY=your_stripe_secret_key

Stripe Client

A Stripe client is created in src/utils/stripe.ts. This client is used to interact with the Stripe API.

src/utils/stripe.ts
import { Stripe } from 'stripe';
import { STRIPE_SECRET_KEY } from '../config';
 
export const stripe = new Stripe(STRIPE_SECRET_KEY, {
  apiVersion: '2020-08-27',
});

Stripe Webhook

A webhook endpoint is set up in src/pages/api/stripe/webhook.ts to handle events from Stripe.

src/pages/api/stripe/webhook.ts
import { errors } from '@/utils/errors';
import { stripe } from '@/utils/stripe';
import {
  manageSubscriptionStatusChange,
  upsertPriceRecord,
  upsertProductRecord,
} from '@/utils/supabase-admin';
import { NextApiRequest, NextApiResponse } from 'next';
import { Readable } from 'node:stream';
import Stripe from 'stripe';
 
// Stripe requires the raw body to construct the event.
export const config = {
  api: {
    bodyParser: false,
  },
};
 
async function buffer(readable: Readable) {
  const chunks: Array<Buffer> = [];
  for await (const chunk of readable) {
    chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
  }
  return Buffer.concat(chunks);
}
 
const relevantEvents = new Set([
  'product.created',
  'product.updated',
  'price.created',
  'price.updated',
  'checkout.session.completed',
  'customer.subscription.created',
  'customer.subscription.updated',
  'customer.subscription.deleted',
]);
/**
 *  Webhook handler which receives Stripe events and updates the database.
 *  Events handled are product.created, product.updated, price.created, price.updated,
 *  checkout.session.completed, customer.subscription.created, customer.subscription.updated.
 *
 *  IMPORTANT! Make sure that when your webshite is deployed, the webhook secret is set in the environment variables and
 *  that the webhook is set up in the Stripe dashboard.
 */
 
const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === 'POST') {
    const buf = await buffer(req);
    const sig = req.headers['stripe-signature'];
    const webhookSecret =
      process.env.STRIPE_WEBHOOK_SECRET_LIVE ??
      process.env.STRIPE_WEBHOOK_SECRET;
    let event: Stripe.Event;
    try {
      if (!sig || !webhookSecret) return;
      event = stripe.webhooks.constructEvent(buf, sig, webhookSecret);
    } catch (error: unknown) {
      errors.add(error);
      if (error instanceof Error) {
        return res.status(400).send(`Webhook error: ${error.message}`);
      }
 
      return res.status(400).send(`Webhook Error: ${String(error)}`);
    }
 
    if (relevantEvents.has(event.type)) {
      try {
        switch (event.type) {
          case 'product.created':
          case 'product.updated':
            await upsertProductRecord(event.data.object as Stripe.Product);
            break;
          case 'price.created':
          case 'price.updated':
            await upsertPriceRecord(event.data.object as Stripe.Price);
            break;
          case 'customer.subscription.created':
          case 'customer.subscription.updated':
          case 'customer.subscription.deleted': {
            const subscription = event.data.object as Stripe.Subscription;
            await manageSubscriptionStatusChange(
              subscription.id,
              subscription.customer as string,
              event.type === 'customer.subscription.created',
            );
            break;
          }
 
          case 'checkout.session.completed': {
            const checkoutSession = event.data
              .object as Stripe.Checkout.Session;
            if (checkoutSession.mode === 'subscription') {
              const subscriptionId = checkoutSession.subscription;
              await manageSubscriptionStatusChange(
                subscriptionId as string,
                checkoutSession.customer as string,
                true,
              );
            }
            break;
          }
          default:
            throw new Error('Unhandled relevant event!');
        }
      } catch (error) {
        errors.add(error);
        return res
          .status(400)
          .send('Webhook error: "Webhook handler failed. View logs."');
      }
    }
 
    res.json({ received: true });
  } else {
    res.setHeader('Allow', 'POST');
    res.status(405).end('Method Not Allowed');
  }
};
 
export default webhookHandler;

Stripe Checkout and Customer Portal

A checkout session is created in src/data/user/organizations.ts.

src/data/user/organizations.ts
export async function createCustomerPortalLinkAction(organizationId: string) {
  'use server';
  const user = await serverGetLoggedInUser();
  const supabaseClient = createSupabaseUserServerActionClient();
  const { data, error } = await supabaseClient
    .from('organizations')
    .select('id, title')
    .eq('id', organizationId)
    .single();
 
  if (error) {
    throw error;
  }
 
  if (!data) {
    throw new Error('Organization not found');
  }
 
  const customer = await createOrRetrieveCustomer({
    organizationId: organizationId,
    organizationTitle: data.title,
    email: user.email || '',
  });
 
  if (!customer) throw Error('Could not get customer');
  const { url } = await stripe.billingPortal.sessions.create({
    customer,
    return_url: toSiteURL(`/organization/${organizationId}/settings/billing`),
  });
 
  return url;
}
 
export async function createCheckoutSessionAction({
  organizationId,
  priceId,
  isTrial = false,
}: {
  organizationId: string;
  priceId: string;
  isTrial?: boolean;
}) {
  'use server';
  const TRIAL_DAYS = 14;
  const user = await serverGetLoggedInUser();
 
  const organizationTitle = await getOrganizationTitle(organizationId);
 
  const customer = await createOrRetrieveCustomer({
    organizationId: organizationId,
    organizationTitle: organizationTitle,
    email: user.email || '',
  });
  if (!customer) throw Error('Could not get customer');
  if (isTrial) {
    const stripeSession = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      billing_address_collection: 'required',
      customer,
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      mode: 'subscription',
      allow_promotion_codes: true,
      subscription_data: {
        trial_period_days: TRIAL_DAYS,
        trial_settings: {
          end_behavior: {
            missing_payment_method: 'cancel',
          },
        },
        metadata: {},
      },
      success_url: toSiteURL(
        `/organization/${organizationId}/settings/billing`,
      ),
      cancel_url: toSiteURL(`/organization/${organizationId}/settings/billing`),
    });
 
    return stripeSession.id;
  } else {
    const stripeSession = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      billing_address_collection: 'required',
      customer,
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      mode: 'subscription',
      allow_promotion_codes: true,
      subscription_data: {
        trial_settings: {
          end_behavior: {
            missing_payment_method: 'cancel',
          },
        },
      },
      metadata: {},
      success_url: toSiteURL(
        `/organization/${organizationId}/settings/billing`,
      ),
      cancel_url: toSiteURL(`/organization/${organizationId}/settings/billing`),
    });
 
    return stripeSession.id;
  }
}

Stripe Subscriptions

Subscriptions are managed in src/utils/supabase-admin.ts. Subscriptions are at the organization level. When a subscription is created, the manageSubscriptionStatusChange function is called. This function creates a new subscription record in the database.

Organization Admins

Organization admins can manage subscriptions in /organization/[organizationId]/settings/billing route. All members can see the current subscription status in /organization/[organizationId]/settings/billing route. However, only organization admins can manage subscriptions.

/organization/[organizationId]/settings/billing.tsx
import { z } from 'zod';
import { OrganizationSubscripionDetails } from './OrganizationSubscripionDetails';
import {
  getLoggedInUserOrganizationRole,
  getNormalizedOrganizationSubscription,
} from '@/data/user/organizations';
import { Suspense } from 'react';
import { T } from '@/components/ui/Typography';
 
const paramsSchema = z.object({
  organizationId: z.string(),
});
 
async function Subscription({ organizationId }: { organizationId: string }) {
  const normalizedSubscription =
    await getNormalizedOrganizationSubscription(organizationId);
  const organizationRole =
    await getLoggedInUserOrganizationRole(organizationId);
  return (
    <OrganizationSubscripionDetails
      organizationId={organizationId}
      organizationRole={organizationRole}
      normalizedSubscription={normalizedSubscription}
    />
  );
}
 
export default async function OrganizationSettingsPage({
  params,
}: {
  params: unknown;
}) {
  const { organizationId } = paramsSchema.parse(params);
  return (
    <Suspense fallback={<T.Subtle>Loading billing details...</T.Subtle>}>
      <Subscription organizationId={organizationId} />;
    </Suspense>
  );
}