---
title: "Understanding Access Control"
description: "Understand how to gate features based on subscription status using Supabase queries and the patterns for checking access."
canonical_url: "https://vercel.com/academy/subscription-store/understanding-access-control"
md_url: "https://vercel.com/academy/subscription-store/understanding-access-control.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T12:34:35.612Z"
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>

# Understanding Access Control

# Understanding Access Control

Users pay for access. You need to gate premium features to subscribers only. This section covers patterns for checking subscription status and controlling access to features, pages, and API routes.

## Outcome

Understand the subscription-based access control pattern and when to use server vs client checks.

## Access Control Patterns

You have two main approaches to feature gating:

| Approach                  | Implementation               | Trade-offs                              |
| ------------------------- | ---------------------------- | --------------------------------------- |
| Check subscription tier   | `if (plan === "elder")`      | Requires code changes when tiers change |
| Check subscription status | `if (hasActiveSubscription)` | Simpler, binary access control          |

For most apps, checking if the user has *any* active subscription is sufficient. You can always add tier-specific logic later.

## When to Check

| Location         | Pattern                                 | Use Case                   |
| ---------------- | --------------------------------------- | -------------------------- |
| Server Component | `await hasActiveSubscription(supabase)` | Page-level access control  |
| Client Component | Pass `hasAccess` as prop from server    | UI conditionals            |
| API Route        | `await hasActiveSubscription(supabase)` | Protect endpoints          |
| Middleware       | Not recommended                         | Too slow for every request |

Server-side checks are preferred because:

- Can't be bypassed by disabling JavaScript
- No flash of unauthorized content
- Keeps subscription logic out of the browser

## Fast Track

1. Review the `hasActiveSubscription()` function in `utils/supabase/queries.ts`
2. Understand server vs client check patterns
3. Plan which resources need protection

## The Access Control Flow

```
User requests protected resource
    ↓
Get user from Supabase auth
    ↓
Query subscriptions table
    ↓
Check if any subscription is active/trialing
    ↓
Grant or deny access
```

## The `hasActiveSubscription` Function

This function (implemented in Section 2) does the heavy lifting:

```typescript title="utils/supabase/queries.ts"
export const hasActiveSubscription = cache(async (supabase: SupabaseClient) => {
  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) return false;

  const { data: subscription } = await supabase
    .from("subscriptions")
    .select("id, status")
    .eq("user_id", user.id)
    .in("status", ["trialing", "active"])
    .maybeSingle();

  return !!subscription;
});
```

This function:

1. Gets the current user from the session
2. Queries for subscriptions with `active` or `trialing` status
3. Returns `true` if any matching subscription exists
4. Uses React's `cache()` to deduplicate requests within a render

## Using the Function

```typescript
// In a Server Component
import { createSupabaseClient } from "@/utils/supabase/server";
import { hasActiveSubscription } from "@/utils/supabase/queries";

export default async function ProtectedPage() {
  const supabase = await createSupabaseClient();
  const hasAccess = await hasActiveSubscription(supabase);

  if (!hasAccess) {
    return <UpgradePrompt />;
  }

  return <PremiumContent />;
}
```

## Done-When

- [ ] Understand when to use server vs client checks
- [ ] Know where `hasActiveSubscription` is implemented
- [ ] Understand the query that checks subscription status
- [ ] Know the difference between checking tier vs checking access

## Server vs Client Checks

| Check Location   | Use Case                 | Security      |
| ---------------- | ------------------------ | ------------- |
| Server Component | Render different content | Authoritative |
| Client Component | Show/hide UI elements    | UX only       |
| API Route        | Gate backend operations  | Authoritative |

**Server-side** checks can't be bypassed - the user never receives the premium content.

**Client-side** checks improve UX (disabled buttons, upgrade prompts) but aren't secure alone.

## Tier-Specific Access (Advanced)

If you need tier-specific logic, use `getSubscription()` instead:

```typescript
const subscription = await getSubscription(supabase);
const productName = subscription?.prices?.products?.name;

if (productName === "Elder") {
  // Elder-only features
} else if (productName === "Ranger") {
  // Ranger features
}
```

But start simple with binary access control and add complexity only when needed.

## Next Steps

In the following lessons, you'll implement:

1. **Server-side checks** - Gate entire pages to subscribers
2. **Client-side checks** - Conditionally render UI elements
3. **API route checks** - Protect API endpoints

Let's start with server-side access control.


---

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