Next.js SaaS Starter

repository·main·Indexed 12 days ago

https://github.com/nextjs/saas-starter

A starter template for building SaaS applications with Next.js. It features built-in authentication using JWTs and cookies, Stripe payment integration for team-based subscriptions, a dashboard with Role-Based Access Control (RBAC), and database management via Drizzle ORM and PostgreSQL.

Tokens
5.3K
Snippets
24
Records
30
Agent score
97%

What's inside Next.js SaaS Starter

  1. Test Stripe payments locally

    main

    To test the payment flow without a real credit card, use the Stripe test card details. You must also have the Stripe CLI installed and logged in (stripe login) to forward webhooks to your local development server.

    Test Card Details:

    • Card Number: 4242 4242 4242 4242
    • Expiration: Any future date
    • CVC: Any 3-digit number

    Forwarding Webhooks: Run the following command to listen for Stripe events and forward them to your local API route:

    stripe listen --forward-to localhost:3000/api/stripe/webhook
  2. Deploy to production and configure Stripe webhooks

    main

    When moving to production, you must configure a production webhook in the Stripe Dashboard to ensure subscription events are processed correctly.

    1. Create a new webhook in the Stripe Dashboard.
    2. Set the endpoint URL to your production API route (e.g., https://yourdomain.com/api/stripe/webhook).
    3. Select required events such as checkout.session.completed and customer.subscription.updated.
  3. Install and set up the Next.js SaaS Starter

    main

    Clone the repository, install dependencies using pnpm, and initialize the database. Use the provided setup script to generate your .env file, then run migrations and seeding to create a default test user.

    Default Test Credentials:

    • User: test@test.com
    • Password: admin123

    Alternatively, you can register new users via the /sign-up route.

    git clone https://github.com/nextjs/saas-starter
    cd saas-starter
    pnpm install
    pnpm db:setup
    pnpm db:migrate
    pnpm db:seed
    pnpm dev
  4. Manage team-based subscriptions and Stripe integration

    main

    The teams table is the primary entity for managing SaaS subscriptions. Instead of individual user billing, the application uses a team-centric model. When integrating with Stripe, you should map the following fields in the teams table:

    • stripeCustomerId: The unique identifier for the Stripe customer.
    • stripeSubscriptionId: The identifier for the active subscription.
    • stripeProductId: The ID of the specific product/plan being used.
    • planName: A human-readable name for the current plan.
    • subscriptionStatus: The current state of the subscription (e.g., active, canceled).
  5. Configure Drizzle ORM

    main

    The project uses drizzle-kit for database migrations and schema management. The configuration is defined in drizzle.config.ts and requires the following settings:

    • schema: The path to the TypeScript file defining your database schema (set to ./lib/db/schema.ts).
    • out: The directory where migration files will be generated (set to ./lib/db/migrations).
    • dialect: The database engine being used (set to postgresql).
    • dbCredentials: An object containing connection details. It requires a url property, which is sourced from the POSTGRES_URL environment variable.
    import type { Config } from 'drizzle-kit';
    
    export default {
      schema: './lib/db/schema.ts',
      out: './lib/db/migrations',
      dialect: 'postgresql',
      dbCredentials: {
        url: process.env.POSTGRES_URL!,
      },
    } satisfies Config;
  6. Configure AUTH_SECRET for session security

    main
    The session management system requires an AUTH_SECRET environment variable. This secret is used as the key for signing and verifying JWTs via the jose library. Ensure this is set in your .env file to prevent authentication bypasses or errors.
  7. Configure production environment variables

    main

    When deploying (e.g., to Vercel), ensure the following environment variables are set in your production environment settings:

    VariableDescription
    BASE_URLYour production domain (e.g., https://yourdomain.com)
    STRIPE_SECRET_KEYYour Stripe secret key for the production environment
    STRIPE_WEBHOOK_SECRETThe secret from the production webhook created in the Stripe Dashboard
    POSTGRES_URLYour production database connection string
    AUTH_SECRETA random string for authentication (generate via openssl rand -base64 32)
    # Generate a secure AUTH_SECRET
    openssl rand -base64 32
  8. Create a Stripe Customer Portal Session

    main

    Use createCustomerPortalSession to allow users to manage their existing subscriptions (e.g., updating payment methods, changing plans, or canceling).

    This function automatically manages the Stripe Billing Portal configuration. If no configuration exists, it creates one that enables:

    • Subscription Updates: Allows changing price, quantity, or using promotion codes with proration.
    • Subscription Cancellation: Allows cancellation at the end of the period with a reason survey.
    • Payment Method Updates: Allows users to update their cards.

    Parameters:

    • team: The Team object containing stripeCustomerId and stripeProductId.
    await createCustomerPortalSession(team);
  9. Hash and compare passwords

    main

    Use hashPassword to create a secure hash of a plain-text password using bcryptjs. Use comparePasswords to verify a plain-text password against a previously generated hash during authentication.

    const hashedPassword = await hashPassword('user-password');
    const isMatch = await comparePasswords('user-password', hashedPassword);
  10. Validate Server Actions with validatedAction

    main

    Use validatedAction to wrap a Server Action with Zod schema validation. It automatically parses the formData, and if validation fails, it returns an ActionState object containing the first error message. If validation succeeds, it calls your action with the parsed data.

    import { z } from 'zod';
    import { validatedAction } from '@/lib/auth/middleware';
    
    const schema = z.object({
      name: z.string().min(1, 'Name is required'),
    });
    
    export const myAction = validatedAction(schema, async (data, formData) => {
      // data is typed as { name: string }
      console.log(data.name);
      return { success: 'Action completed' };
    });
  11. Retrieve Stripe Prices and Products

    main

    Use these functions to fetch active subscription data from Stripe to populate your pricing UI.

    • getStripePrices(): Returns an array of recurring prices. Each object includes id, productId, unitAmount, currency, interval, and trialPeriodDays.
    • getStripeProducts(): Returns an array of active products. Each object includes id, name, description, and defaultPriceId.
    const prices = await getStripePrices();
    const products = await getStripeProducts();
  12. Trigger a Stripe checkout session with checkoutAction

    main

    Use checkoutAction to initiate a Stripe checkout process for a specific team. This action is wrapped with withTeam middleware, meaning it requires a valid team context to execute. It expects a formData object containing a priceId key representing the Stripe Price ID to be charged.

    // Example usage in a Client Component form
    <form action={checkoutAction}>
      <input type="hidden" name="priceId" value="price_12345" />
      <button type="submit">Subscribe</button>
    </form>