---
title: "Server-Side Checks"
description: "Check subscription status in Server Components to conditionally render premium content or show upgrade prompts for users without access."
canonical_url: "https://vercel.com/academy/subscription-store/server-side-subscription-checks"
md_url: "https://vercel.com/academy/subscription-store/server-side-subscription-checks.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T17:27:58.770Z"
content_type: "lesson"
course: "subscription-store"
course_title: "Launch a Subscription Store with Vercel and Stripe"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Server-Side Checks

# 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

1. Create `app/protected/paid-content/page.tsx`
2. Check subscription with `hasActiveSubscription(supabase)`
3. 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:**

1. Create paid content page at `/protected/paid-content`
2. Check subscription status server-side with `hasActiveSubscription()`
3. Render premium content for subscribers
4. Render upgrade prompt for non-subscribers
5. 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

1. **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

2. **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

3. **Verify security:**
   - View page source on the upgrade prompt page
   - The premium content markup should not be present at all

## Commit

```bash
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`:

```typescript title="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** - `FieldGuideCard` is only rendered for subscribers

### Step 3: Add Navigation Link

Update your navigation or sidebar to include a link to the paid content page:

```typescript
<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:**

```html
<div>
  <h1>Field Guide</h1>
  <p>Discover edible plants and mushrooms...</p>
  <!-- FieldGuideCard content here -->
</div>
```

**Without Subscription:**

```html
<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 `active` or `trialing`
- Check the `subscriptions` table 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


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
