Vercel Logo

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:

ApproachImplementationTrade-offs
Check subscription tierif (plan === "elder")Requires code changes when tiers change
Check subscription statusif (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

LocationPatternUse Case
Server Componentawait hasActiveSubscription(supabase)Page-level access control
Client ComponentPass hasAccess as prop from serverUI conditionals
API Routeawait hasActiveSubscription(supabase)Protect endpoints
MiddlewareNot recommendedToo 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:

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

// 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 LocationUse CaseSecurity
Server ComponentRender different contentAuthoritative
Client ComponentShow/hide UI elementsUX only
API RouteGate backend operationsAuthoritative

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:

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.