Server-Side Subscription Checks
Server-side checks are authoritative. When you check subscriptions in a Server Component, users without access never receive the premium content - it's not hidden with CSS or JavaScript, it simply doesn't exist in their response. This is the secure foundation for access control.
Outcome
Gate Server Components based on subscription status, rendering premium content or upgrade prompts.
Fast Track
- Create
app/protected/paid-content/page.tsx - Check subscription with
hasActiveSubscription(supabase) - Render premium content or upgrade prompt based on result
Hands-on Exercise 3.2
Build a paid content page with server-side subscription checks:
Requirements:
- Create paid content page at
/protected/paid-content - Check subscription status server-side with
hasActiveSubscription() - Render premium content for subscribers
- Render upgrade prompt for non-subscribers
- Add loading skeleton
Implementation hints:
- Use the Supabase server client in an async Server Component
hasActiveSubscription()returns a boolean- Return early with different JSX for each state
- Include a link to the pricing page in the upgrade prompt
Try It
-
Without subscription:
- Sign out and create a new account (or use one without a subscription)
- Visit http://localhost:3000/protected/paid-content
- You should see upgrade prompt with link to pricing
-
With subscription:
- Sign in with an account that has a subscription
- Visit http://localhost:3000/protected/paid-content
- You should see the premium Field Guide content
-
Verify security:
- View page source on the upgrade prompt page
- The premium content markup should not be present at all
Commit
git add -A
git commit -m "feat(access): add server-side subscription checks"Done-When
- Paid content page renders at
/protected/paid-content - Subscription check runs server-side
- Premium content shows for subscribers
- Upgrade prompt shows for non-subscribers
- Loading skeleton displays during fetch
Solution
Step 1: Create Loading State
The loading state is already in the starter at app/protected/paid-content/loading.tsx.
Step 2: Create Paid Content Page
Update app/protected/paid-content/page.tsx:
import { createSupabaseClient } from "@/utils/supabase/server";
import { hasActiveSubscription } from "@/utils/supabase/queries";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import FieldGuideCard from "@/components/field-guide-card";
import Link from "next/link";
export default async function PaidContent() {
const supabase = await createSupabaseClient();
const hasAccess = await hasActiveSubscription(supabase);
if (!hasAccess) {
return (
<div className="flex min-h-[400px] items-center justify-center">
<Card className="p-6">
<h2 className="text-xl font-semibold">Rangers & Elders Only</h2>
<p className="mt-2 text-muted-foreground">
The Field Guide is available to Ranger and Elder members. Upgrade
your membership to access our complete database of edible plants,
mushrooms, and foraging guides.
</p>
<Button className="mt-4" variant="outline" asChild>
<Link href="/protected/pricing">Upgrade Membership</Link>
</Button>
</Card>
</div>
);
}
return (
<div>
<div>
<h1 className="text-2xl font-medium">Field Guide</h1>
<p className="text-muted-foreground mt-2">
Discover edible plants and mushrooms from our curated database
</p>
</div>
<FieldGuideCard className="mt-4" />
</div>
);
}Key patterns:
- Server Component - No "use client" directive, runs on the server
hasActiveSubscription()- Queries Supabase for active subscriptions- Early return - Different JSX for each access state
- Premium component -
FieldGuideCardis only rendered for subscribers
Step 3: Add Navigation Link
Update your navigation or sidebar to include a link to the paid content page:
<Link href="/protected/paid-content">Field Guide</Link>How Server-Side Checks Work
Browser requests /protected/paid-content
↓
Next.js calls PaidContent (Server Component)
↓
createSupabaseClient() → server client with session
↓
hasActiveSubscription(supabase)
↓
Query subscriptions table for active/trialing status
↓
Returns true/false
↓
Server Component renders appropriate JSX
↓
Only rendered HTML sent to browser
Users without access never receive the premium content - it's not in the HTML, JavaScript bundle, or anywhere in their response.
Response Comparison
With Subscription:
<div>
<h1>Field Guide</h1>
<p>Discover edible plants and mushrooms...</p>
<!-- FieldGuideCard content here -->
</div>Without Subscription:
<div class="flex min-h-[400px] items-center justify-center">
<div class="p-6">
<h2>Rangers & Elders Only</h2>
<p>The Field Guide is available to Ranger and Elder members...</p>
<a href="/protected/pricing">Upgrade Membership</a>
</div>
</div>The premium content simply doesn't exist in the second response.
File Structure After This Lesson
app/protected/
├── page.tsx ← Account page
├── layout.tsx ← Protected layout
├── pricing/
│ ├── page.tsx
│ └── loading.tsx
├── subscription/
│ ├── page.tsx
│ └── loading.tsx
└── paid-content/
├── page.tsx ← Updated: subscription-gated page
└── loading.tsx ← Loading skeleton
components/
└── field-guide-card.tsx ← Premium content component
Troubleshooting
Always shows upgrade prompt:
- Verify you have an active subscription (check
/protected/subscription) - The subscription status must be
activeortrialing - Check the
subscriptionstable in Supabase directly
Always shows premium content:
- The user may have an active subscription you forgot about
- Check subscription status in Stripe dashboard
- Query the subscriptions table with the user's ID
Page shows loading skeleton forever:
- Check browser console for errors
- Verify Supabase environment variables are set
- Ensure the user is authenticated
Was this helpful?