Content Gating
Gate premium content with persisted billing access.
The template can protect a documentation page with the customer's persisted lifetime purchase or active subscription. Checkout redirects do not unlock the page. The docs route reads billing state on the server before it renders the protected content.
Try the Shipped Example
The template includes Premium Example as a working gated page.
Start the app
bun run devVisit without access
Open /docs/customization/premium-example in a signed-out browser or with a new
account. The page title and preview remain visible, but the protected section is
replaced by the paywall.
Complete a test purchase
Run the documented lifetime or subscription billing path, then return to the same URL. The protected section now renders from the server.
Gate a Page
Add gated: true to the page frontmatter:
---
title: Premium Guide
description: A guide for paying customers.
gated: true
---
## Protected guide content
Only customers with persisted billing access can read this section.The frontmatter schema already accepts the gated boolean. No separate route
list or client-side flag is required for a single page.
How the Decision Works
src/lib/gating.ts resolves the requested docs page and reads its frontmatter:
export function isPageGated(slug: string[] | undefined): boolean {
if (!slug || slug.length === 0) return false;
const page = source.getPage(slug);
return page?.data.gated === true;
}The docs route combines that page rule with the server-side purchase reader:
const gated = isPageGated(params.slug);
const purchaseStatus = await getUserPurchaseStatus();
const showPaywall = gated && !purchaseStatus?.hasPurchase;
return showPaywall ? <GatedContent /> : <MDX />;hasPurchase is true only for a persisted lifetime purchase or an active or
trialing subscription. Authentication by itself does not unlock the page.
Do not gate content with a browser-only flag, Checkout success query, or hidden CSS. Keep the access decision on the server and backed by the billing reader.
Gate a Folder
Page frontmatter is the shipped default. To gate a whole folder, extend the
gatedPaths list inside src/lib/gating.ts with slug segments:
const gatedPaths = [
['premium'],
['advanced', 'billing'],
];Each entry matches that folder and its child pages. Keep frontmatter for exceptions that do not belong to a fully protected folder.
Prove the Gate
The billing suites use the shipped premium example as the final reader. Each suite creates a new user, confirms the paywall before Checkout, completes the signed webhook path, and then confirms the protected section is visible:
./coremvp e2e billing:lifetime
./coremvp e2e billing:subscriptionAfter deployment, run the hosted equivalent against Stripe test mode:
./coremvp e2e billing:hostedRelated
- Payments - Configure and prove billing
- Authentication - Understand sessions
- Premium Example - Inspect the shipped gated page
Was this page helpful?