Docs
Authentication
Protected Pages

Setting Up Protected Pages

Protected pages are essential for granting users access to your product once they become paying customers. For a user to access these protected pages, they must be signed in and have an active plan.

How to Determine if a User Has an Active Plan

StartupBolt uses a credit-based system to manage access to your product. When a user buys a plan, they receive a certain number of credits. By default, the number of credits is 100, but you can customize this value to suit your needs.

If a user's credits are higher than 0, they will have access to the product. This approach offers several advantages:

  • For AI apps or services, usage can be tied to credits, offering flexibility and control.
  • For regular SaaS apps, you can use the system as a boolean-like mechanism:
    • 100 credits = access granted.
    • 0 credits = access denied.
  • This makes the system suitable for both normal SaaS apps and AI/credit-based apps.

All these logics are handled by our payment systems, which currently support Stripe and LemonSqueezy for payments.

Please refer to the Credits Guide for more details on how to configure and manage credits.

Please refer to the Stripe Guide or LemonSqueezy Guide for more details on how to configure and manage payments.

How Protected Pages Work

StartupBolt implements credit-based access control through two key utilities that work together to protect your routes and content:

  1. Credit Verification Utility (fetchCredits)
  2. Credit Authorization Component (RequireCredits)

Protected Routes

By default, all routes under /private are protected. But you can protect any route or component by using these utilities in your layouts or pages. Here's a typical usage pattern:

import { fetchCredits } from "@/utils/fetchCredits";
import { RequireCredits } from "@/utils/components/RequireCredits";
 
// In your server component:
const { credits, isCustomer } = await fetchCredits();
 
// Then protect your content:
<RequireCredits credits={credits} isCustomer={isCustomer} showWhen="hasCredits">
  <YourProtectedContent />
</RequireCredits>

Credit Verification Utility

The fetchCredits utility handles authentication and credit verification:

import { fetchCredits } from "@/utils/fetchCredits";
const { credits, isCustomer } = await fetchCredits();

This server-side utility:

  • Verifies user authentication via Supabase
  • Checks customer status
  • Returns credit balance and customer status
  • Handles edge cases for non-authenticated users

Credit Authorization Component

The RequireCredits component provides granular access control through five different states:

<RequireCredits 
  credits={credits} 
  isCustomer={isCustomer} 
  showWhen="hasCredits"
>
  {/* Protected content */}
</RequireCredits>

Available states:

StateDescriptionWhen to Use
hasCreditsUser has active creditsMain product features
noCreditsNo credits (guest or expired)Upgrade prompts
customerWithNoCreditsCustomer with expired creditsRenewal messages
guestNew user, never purchasedOnboarding content
customerAny registered customerCustomer-only features
  • hasCredits and noCredits are the most commonly used states. But if you want to be more granular, you can use the remaining three states.

Access Control Example

Here's a practical example of implementing tiered access:

export default async function ProtectedPage() {
  const { credits, isCustomer } = await fetchCredits();
  
  return (
    <>
      {/* Active subscribers see this */}
      <RequireCredits credits={credits} isCustomer={isCustomer} showWhen="hasCredits">
        <MainProduct />
      </RequireCredits>
 
      {/* Non-customers see this */}
      <RequireCredits credits={credits} isCustomer={isCustomer} showWhen="guest">
        <PricingTable />
      </RequireCredits>
 
      {/* Expired customers see this */}
      <RequireCredits credits={credits} isCustomer={isCustomer} showWhen="customerWithNoCredits">
        <RenewalPrompt />
      </RequireCredits>
    </>
  );
}

Customizing the Base Protected Route

The base protected route (/private by default) is where users are redirected after purchase or login. To customize it:

  1. Rename the "private" folder to your desired name (e.g., "dashboard")
  2. Update URLS.protected in settings.js to match the new route name
protected_pages url settings

Detailed Instructions

For more detailed instructions, refer to the comments inside the following:

  • Layout located at app/private/layout.js.
  • Page located at app/private/page.js.
  • Utility function located at utils/fetchCredits.js.
  • Utility component located at utils/components/RequireCredits.js.
protected_pages

Default Protected Pages

StartupBolt provides default protected page implementations that you can use as a starting point. Let's examine each component and understand how they work together.

Default Protected Layout

The default protected layout (app/private/layout.js) handles authentication and credit-based access control. Only customers with credits will be able to access the protected content.

import Navbar from "@/modules/native/Navbar";
import PricingTable from "@/modules/native/PricingTable";
import MiniFooter from "@/modules/native/MiniFooter";
import { redirect } from "next/navigation";
import { createClient } from '@/utils/supabase/server';
import settings from "@/settings";
import { fetchCredits } from "@/utils/fetchCredits";
import { RequireCredits } from "@/utils/components/RequireCredits";
 
const NotACustomer = () => (
  <>
    <Navbar navigation={null} cta={null} />
    <main className="flex-1 bg-background text-foreground">
      <PricingTable
        heading="You do not have an active plan."
        description="Please purchase a plan to continue using our services."
      />
    </main>
    <MiniFooter
      sectionColors={{
        background: "bg-accent",
        foreground: "text-accent-foreground"
      }}
    />
  </>
);
 
export default async function PrivateLayout({ children }) {
  // 1. Check authentication
  const supabase = createClient();
  const { data: { user }, error: userError } = await supabase.auth.getUser();
 
  if (userError || !user) {
    redirect(settings.URLS.login);
  }
 
  // 2. Verify credits and customer status
  const { credits, isCustomer } = await fetchCredits();
 
  // 3. Render protected content or upgrade prompt
  return (
    <>
      <RequireCredits credits={credits} isCustomer={isCustomer} showWhen="hasCredits">
        {children}
      </RequireCredits>
      <RequireCredits credits={credits} isCustomer={isCustomer} showWhen="noCredits">
        <NotACustomer />
      </RequireCredits>
    </>
  );
}

Default Protected Page

The default protected page (app/private/page.js) provides a simple welcome screen with credit information:

import Navbar from "@/modules/native/Navbar";
import MiniFooter from "@/modules/native/MiniFooter";
import { fetchCredits } from "@/utils/fetchCredits";
 
export const dynamic = "force-dynamic";
 
export default async function Private() {
  const { credits } = await fetchCredits();
 
  return (
    <>
      <Navbar
        navigation={null}
        cta={null}
      />
      <main className="flex-1 bg-background text-foreground">
        <section className="container mx-auto flex flex-col items-center px-4 sm:px-8 py-12 sm:py-24">
          <h1 className="font-bold text-4xl leading-tight lg:text-5xl lg:leading-tight mb-12 flex-row flex-wrap text-center justify-center max-w-7xl">Premium Page</h1>
          <p className="text-lg text-center mb-12 max-w-xl">You have {credits} credits remaining. Enjoy your premium access!</p>
        </section>
      </main>
      <MiniFooter
        sectionColors={{
          background: "bg-accent",
          foreground: "text-accent-foreground"
        }}
      />
    </>
  );
}

Example: Granular Access Control

Here's how to implement more granular access control in your protected pages:

Layout with Basic Authentication

This layout only handles authentication, leaving credit checks to individual pages:

import { redirect } from "next/navigation";
import { createClient } from '@/utils/supabase/server';
import settings from "@/settings";
 
export default async function PrivateLayout({ children }) {
  const supabase = createClient();
  const { data: { user }, error: userError } = await supabase.auth.getUser();
 
  if (userError || !user) {
    redirect(settings.URLS.login);
  }
 
  return <>{children}</>;
}

Page with Granular Access Control

This example shows how to render different content based on credit and user status:

import Navbar from "@/modules/native/Navbar";
import PricingTable from "@/modules/native/PricingTable";
import MiniFooter from "@/modules/native/MiniFooter";
import { fetchCredits } from "@/utils/fetchCredits";
import { RequireCredits } from "@/utils/components/RequireCredits";
 
export const dynamic = "force-dynamic";
 
export default async function Private() {
  const { credits, isCustomer } = await fetchCredits();
 
  return (
    <>
      <Navbar navigation={null} cta={null} />
      <main className="flex-1 bg-background text-foreground">
        {/* Premium Content */}
        <RequireCredits credits={credits} isCustomer={isCustomer} showWhen="hasCredits">
          <section className="container mx-auto flex flex-col items-center px-4 sm:px-8 py-12 sm:py-24">
            <h1 className="font-bold text-4xl leading-tight lg:text-5xl lg:leading-tight mb-12 flex-row flex-wrap text-center justify-center max-w-7xl">Premium Page</h1>
            <p className="text-lg text-center mb-12 max-w-xl">You have {credits} credits remaining. Enjoy your premium access!</p>
          </section>
        </RequireCredits>
 
        {/* Upgrade Prompt */}
        <RequireCredits credits={credits} isCustomer={isCustomer} showWhen="noCredits">
          <PricingTable
            heading="You do not have an active plan."
            description="Please purchase a plan to continue using our services."
          />
        </RequireCredits>
      </main>
      <MiniFooter sectionColors={{
        background: "bg-accent",
        foreground: "text-accent-foreground"
      }} />
    </>
  );
}

This approach lets you:

  • Handle authentication at the layout level
  • Control access to specific features within pages
  • Show different content based on user status
  • Maintain a consistent upgrade flow

Now you know how to set up protected pages for your product, ensuring access is granted only to paying customers.