Skip to main content
RapidDev - Software Development Agency

How to Build Membership site with V0

Build a gated content membership site with V0 using Next.js, Supabase, Stripe, and shadcn/ui. Free users see teasers, paid members access premium content based on their tier, and Stripe webhooks automatically sync subscription status for real-time access control. Takes about 1-2 hours.

What you'll build

  • Public landing page with pricing table and Stripe Checkout integration for plan selection
  • Content library with Lock icon overlay on gated items and tier-based access control
  • Stripe webhook handler syncing subscription status on create, update, and cancel events
  • Account management page with subscription details and Stripe billing portal redirect
  • Content progress tracking for members with completed/in-progress indicators
  • Paywall component that shows teaser content and upgrade CTA for insufficient tier levels
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate10 min read1-2 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

Build a gated content membership site with V0 using Next.js, Supabase, Stripe, and shadcn/ui. Free users see teasers, paid members access premium content based on their tier, and Stripe webhooks automatically sync subscription status for real-time access control. Takes about 1-2 hours.

What you're building

Creators, coaches, and community builders monetize their knowledge through membership sites with tiered content access. A custom-built membership platform gives you full control over pricing, content gates, and the member experience without Patreon's 8-12% cut.

V0 generates the pricing page, content library, and paywall components from prompts. Stripe via the Vercel Marketplace handles recurring subscriptions with automatic key provisioning. Supabase stores membership records and content.

The architecture uses Stripe webhooks to sync subscription status in real-time, a Server Component content page that checks membership tier before rendering, revalidatePath in the webhook handler to bust cached pages, and Stripe's billing portal for self-service subscription management.

Final result

A membership site with tiered content access, Stripe subscription billing, webhook-synced membership status, and a self-service billing portal.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
StripePayments

Prerequisites

  • A V0 account (Premium or higher recommended)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • A Stripe account with products and prices created (test mode works)
  • Your content and tier structure defined (what is free, basic, premium)

Build steps

1

Set up the database schema for plans, memberships, and content

Create the Supabase schema linking Stripe subscription data to content tier requirements. Each content piece has a tier_required level that determines which members can access it.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a membership site. Create a Supabase schema:
3// 1. plans: id (uuid PK), name (text), stripe_price_id (text), tier (int), features (text[]), is_active (boolean DEFAULT true)
4// 2. memberships: id (uuid PK), user_id (uuid FK to auth.users), plan_id (uuid FK to plans), stripe_subscription_id (text), status (text DEFAULT 'active'), current_period_end (timestamptz)
5// 3. content: id (uuid PK), title (text), slug (text UNIQUE), body (text), excerpt (text), tier_required (int DEFAULT 0), category (text), published_at (timestamptz), author_id (uuid FK to auth.users)
6// 4. content_progress: user_id (uuid FK to auth.users), content_id (uuid FK to content), completed (boolean DEFAULT false), last_accessed (timestamptz), PRIMARY KEY (user_id, content_id)
7// Seed plans: Free (tier 0), Basic (tier 1), Premium (tier 2).
8// Add RLS policies. Generate SQL and TypeScript types.

Pro tip: Use V0's Stripe integration via Vercel Marketplace to auto-provision STRIPE_SECRET_KEY and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY into the Vars tab.

Expected result: Supabase is connected with plans seeded, content table with tier gating, and Stripe keys in the Vars tab.

2

Build the landing page with pricing table

Create the public landing page with a hero section and pricing table. Each plan card links to Stripe Checkout for subscription creation.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create a membership landing page at app/page.tsx.
3// Requirements:
4// - Hero section: headline about premium content, subheading, CTA Button
5// - Pricing section with shadcn/ui Cards for each plan:
6// - Plan name, price/month, tier Badge, features list with checkmarks
7// - 'Subscribe' Button on paid plans → creates Stripe Checkout session
8// - 'Get Started' on free plan → sign up with Supabase Auth
9// - Highlight the recommended plan with a border and 'Popular' Badge
10// - FAQ section with Accordion: billing questions, cancellation, content access
11// - Separator between sections
12// - Server Action for creating Stripe Checkout subscription session

Expected result: The landing page shows pricing cards with Stripe Checkout buttons that create subscription sessions.

3

Create the content library with tier-based access control

Build the content library page and individual article pages with server-side tier checking. Content above the user's tier shows a paywall with an upgrade CTA.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create content pages:
3// 1. app/content/page.tsx — content library with Card grid. Each Card shows: title, excerpt, category Badge, tier Badge. If user's membership tier < content tier_required, show a Lock icon overlay and 'Upgrade to Access' Badge.
4// 2. app/content/[slug]/page.tsx — Server Component that:
5// a. Fetches the content by slug
6// b. Checks the user's membership tier from the memberships table
7// c. If tier >= tier_required, renders the full article body
8// d. If tier < tier_required, renders the excerpt + a Paywall component:
9// - Shows excerpt text fading out with gradient overlay
10// - 'Unlock this content' Card with the required plan name and Subscribe Button
11// e. Tracks content_progress (last_accessed timestamp) for authenticated users
12// - Add category filter Tabs and search Input on the library page

Expected result: The content library shows all content with lock indicators. Article pages check tier and show full content or a paywall.

4

Build the Stripe webhook for subscription sync

Create the webhook handler that keeps membership status in sync with Stripe. When subscriptions are created, updated, or cancelled, the membership record is updated and cached pages are revalidated.

app/api/webhooks/stripe/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import Stripe from 'stripe'
3import { createClient } from '@supabase/supabase-js'
4import { revalidatePath } from 'next/cache'
5
6const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
7const supabase = createClient(
8 process.env.SUPABASE_URL!,
9 process.env.SUPABASE_SERVICE_ROLE_KEY!
10)
11
12export async function POST(req: NextRequest) {
13 const rawBody = await req.text()
14 const sig = req.headers.get('stripe-signature')!
15
16 let event: Stripe.Event
17 try {
18 event = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET!)
19 } catch {
20 return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
21 }
22
23 const subscription = event.data.object as Stripe.Subscription
24 const userId = subscription.metadata?.user_id
25
26 if (!userId) return NextResponse.json({ received: true })
27
28 switch (event.type) {
29 case 'customer.subscription.created':
30 case 'customer.subscription.updated': {
31 const priceId = subscription.items.data[0]?.price.id
32 const { data: plan } = await supabase
33 .from('plans')
34 .select('id')
35 .eq('stripe_price_id', priceId)
36 .single()
37
38 await supabase.from('memberships').upsert({
39 user_id: userId,
40 plan_id: plan?.id,
41 stripe_subscription_id: subscription.id,
42 status: subscription.status === 'active' ? 'active' : 'inactive',
43 current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
44 })
45 break
46 }
47 case 'customer.subscription.deleted': {
48 await supabase.from('memberships')
49 .update({ status: 'cancelled' })
50 .eq('stripe_subscription_id', subscription.id)
51 break
52 }
53 }
54
55 revalidatePath('/content')
56 return NextResponse.json({ received: true })
57}

Expected result: Subscription changes in Stripe automatically update the membership table and revalidate cached content pages.

5

Add account management and deploy

Build the member account page with subscription details and a Stripe billing portal link for self-service management (upgrade, downgrade, cancel).

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create an account page at app/account/page.tsx.
3// Requirements:
4// - Fetch the current user's membership with plan details
5// - Show a Card with: plan name, tier Badge, status Badge (active=green, cancelled=red), current_period_end date
6// - 'Manage Subscription' Button that creates a Stripe billing portal session and redirects
7// - Content progress section: Table showing accessed content with completed checkmarks and last_accessed dates
8// - If no membership, show a CTA Card: 'Join a plan to access premium content' with link to pricing
9// - Server Action for creating the Stripe billing portal session
10
11// The Server Action:
12'use server'
13import Stripe from 'stripe'
14import { redirect } from 'next/navigation'
15
16const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
17
18export async function createPortalSession(customerId: string) {
19 const session = await stripe.billingPortal.sessions.create({
20 customer: customerId,
21 return_url: `${process.env.NEXT_PUBLIC_APP_URL}/account`,
22 })
23 redirect(session.url)
24}

Pro tip: Register webhook events in Stripe Dashboard: customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, and invoice.payment_failed.

Expected result: Members can view their subscription status and manage billing through Stripe's hosted portal. The app is deployed to Vercel.

Complete code

app/api/webhooks/stripe/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import Stripe from 'stripe'
3import { createClient } from '@supabase/supabase-js'
4import { revalidatePath } from 'next/cache'
5
6const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
7const supabase = createClient(
8 process.env.SUPABASE_URL!,
9 process.env.SUPABASE_SERVICE_ROLE_KEY!
10)
11
12export async function POST(req: NextRequest) {
13 const rawBody = await req.text()
14 const sig = req.headers.get('stripe-signature')!
15
16 let event: Stripe.Event
17 try {
18 event = stripe.webhooks.constructEvent(
19 rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET!
20 )
21 } catch {
22 return NextResponse.json({ error: 'Invalid sig' }, { status: 400 })
23 }
24
25 const sub = event.data.object as Stripe.Subscription
26 const userId = sub.metadata?.user_id
27 if (!userId) return NextResponse.json({ received: true })
28
29 if (
30 event.type === 'customer.subscription.created' ||
31 event.type === 'customer.subscription.updated'
32 ) {
33 const priceId = sub.items.data[0]?.price.id
34 const { data: plan } = await supabase
35 .from('plans')
36 .select('id')
37 .eq('stripe_price_id', priceId)
38 .single()
39
40 await supabase.from('memberships').upsert({
41 user_id: userId,
42 plan_id: plan?.id,
43 stripe_subscription_id: sub.id,
44 status: sub.status === 'active' ? 'active' : 'inactive',
45 current_period_end: new Date(
46 sub.current_period_end * 1000
47 ).toISOString(),
48 })
49 }
50
51 if (event.type === 'customer.subscription.deleted') {
52 await supabase.from('memberships')
53 .update({ status: 'cancelled' })
54 .eq('stripe_subscription_id', sub.id)
55 }
56
57 revalidatePath('/content')
58 return NextResponse.json({ received: true })
59}

Customization ideas

Drip content scheduling

Release content on a schedule after membership start date, unlocking new articles weekly to keep members engaged over time.

Community discussion per article

Add a comments section below each content piece where members can discuss and ask questions, using Supabase Realtime for live updates.

Content completion certificates

Generate PDF certificates when members complete all content in a category, using @react-pdf/renderer for server-side PDF creation.

Free trial period

Add a 7-day free trial by setting trial_period_days in the Stripe Checkout session and showing trial status in the account page.

Common pitfalls

Pitfall: Checking membership status only at build time, not on each request

How to avoid: Use revalidatePath('/content') in the Stripe webhook handler to bust the cache when subscription status changes. Or use dynamic rendering for gated content pages.

Pitfall: Using request.json() instead of request.text() in the Stripe webhook

How to avoid: Always use request.text() and pass the raw body to stripe.webhooks.constructEvent().

Pitfall: Gating content only with client-side checks

How to avoid: Check membership tier in the Server Component before rendering content. Only pass the body prop to the article component if the user's tier is sufficient.

Best practices

  • Check membership tier in Server Components before rendering premium content — never rely on client-side gating alone
  • Use revalidatePath in the webhook handler to bust cached content pages when subscription status changes
  • Use Stripe's billing portal for subscription management — it handles upgrades, downgrades, and cancellations with zero code
  • Store stripe_price_id in the plans table to map Stripe subscriptions to your tier system
  • Always use request.text() for Stripe webhook raw body verification
  • Use V0's Design Mode (Option+D) to adjust pricing card layouts and paywall styling without spending credits
  • Register all relevant webhook events: customer.subscription.created, updated, deleted, and invoice.payment_failed
  • Use ISR with revalidate for content pages to balance performance with freshness

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a membership site with Next.js App Router, Supabase, and Stripe. I need a Stripe webhook handler that syncs subscription status. When customer.subscription.created or updated fires, it should look up the plan by stripe_price_id, upsert the membership record, and call revalidatePath to bust cached content pages. When customer.subscription.deleted fires, it should mark the membership as cancelled. Please write the complete webhook route at app/api/webhooks/stripe/route.ts.

Build Prompt

Create a Paywall component that wraps premium content. If the user has sufficient tier, render the full content. If not, show the excerpt with a gradient fade overlay and a Card below it saying 'This content requires [Plan Name] membership' with a Subscribe Button. The component should accept tier_required, user_tier, excerpt, and children (full content) as props. Make it a Server Component that conditionally renders children.

Frequently asked questions

How does content gating work?

Each content piece has a tier_required number (0=free, 1=basic, 2=premium). The content page Server Component checks the user's membership tier from Supabase. If their tier is high enough, it renders the full article. Otherwise, it shows the excerpt with a paywall component prompting an upgrade.

What happens when a subscription is cancelled?

Stripe sends a customer.subscription.deleted webhook. The handler updates the membership status to 'cancelled' and calls revalidatePath to bust cached content pages. The member loses access to gated content immediately.

Can members manage their own subscriptions?

Yes. The account page has a 'Manage Subscription' button that creates a Stripe billing portal session. Members can upgrade, downgrade, cancel, update payment methods, and view invoices through Stripe's hosted portal.

Do I need a paid V0 plan?

Premium ($20/month) is recommended. The membership site has multiple pages (landing, content library, article with paywall, account, webhook handler) that require several prompts.

Can I add a free trial period?

Yes. Add trial_period_days: 7 to the Stripe Checkout session creation. During the trial, the membership status is 'trialing' and the user has full access. The webhook handles the transition to 'active' or 'cancelled' when the trial ends.

How do I deploy the membership site?

Click Share in V0, then Publish to Production. Set STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY in the Vars tab. Register webhook events in Stripe Dashboard pointing to your production URL.

Can RapidDev help build a custom membership site?

Yes. RapidDev has built over 600 apps including membership platforms with tiered content, drip scheduling, and community features. Book a free consultation to plan your membership business.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.