CoreMVP
Customization

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 dev

Visit 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:

src/content/docs/premium-guide.mdx
---
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:

src/lib/gating.ts
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:

src/app/docs/[[...slug]]/page.tsx
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:

src/lib/gating.ts
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:subscription

After deployment, run the hosted equivalent against Stripe test mode:

./coremvp e2e billing:hosted

Was this page helpful?

On this page