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 treats a page as protected when its frontmatter has
gated: true or its slug matches a configured folder prefix. The same decision
protects both the rendered page and its raw Markdown reader.
The server-only Docs renderer checks access before rendering the MDX body:
const gated = isPageGated(slug);
const hasPurchase = gated && (await getUserPurchaseStatus())?.hasPurchase;
const showPaywall = gated && !hasPurchase;hasPurchase is true only for a persisted lifetime purchase or an active or
trialing subscription. Authentication by itself does not unlock the page.
Without access, the loader returns no protected body and an empty table of
contents. It sends the public page tree and preview so the route can show the
paywall. Authorized MDX renders through Start's server component transport;
private page bodies stay out of the browser JavaScript. Gated responses use
Cache-Control: private, no-store.
The raw Markdown route in src/routes/llms[.]mdx/docs/$.ts checks the same
access independently. A direct Markdown request cannot bypass the page gate.
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. The lifetime suite starts with guest Checkout and completes onboarding; the subscription suite signs up before Checkout. Both confirm the paywall first, complete the signed webhook path, and then confirm the protected section is visible:
./coremvp e2e billing:lifetime
./coremvp e2e billing:subscriptionSee the hosted deployment status before using the hosted test commands. After deployment, the production equivalent targets Stripe test mode:
Put the deployed project's NEXT_PUBLIC_SUPABASE_URL and matching
SUPABASE_SECRET_KEY in ignored .env for signup-mode subscription
proof. For lifetime test_capture, put the same TEST_EMAIL_CAPTURE_TOKEN in
ignored local .env and the Vercel Production proof target. Never print these
values, and stop if they do not match the deployed app. Follow
Deploy to Vercel to retrieve the secret key,
restore the retained sensitive capture token, and verify the deployed capture
mode from a fresh checkout.
export TEST_BASE_URL=https://<project-name>.vercel.app
./coremvp prod e2e billing:lifetime
export PROD_E2E_AUTH_MODE=signup
./coremvp prod e2e billing:subscriptionProduction lifetime proof requires the protected test-capture mode used for guest onboarding. Production subscription proof signs in or signs up before Checkout and does not use that capture path.
Related
- Payments - Configure and prove billing
- Authentication - Understand sessions
- Premium Example - Inspect the shipped gated page
Was this page helpful?