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:
- Credit Verification Utility (
fetchCredits
) - 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:
State | Description | When to Use |
---|---|---|
hasCredits | User has active credits | Main product features |
noCredits | No credits (guest or expired) | Upgrade prompts |
customerWithNoCredits | Customer with expired credits | Renewal messages |
guest | New user, never purchased | Onboarding content |
customer | Any registered customer | Customer-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:
- Rename the "private" folder to your desired name (e.g., "dashboard")
- Update
URLS.protected
insettings.js
to match the new route name
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
.
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.