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
- Review the
hasActiveSubscription()function inutils/supabase/queries.ts - Understand server vs client check patterns
- 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:
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:
- Gets the current user from the session
- Queries for subscriptions with
activeortrialingstatus - Returns
trueif any matching subscription exists - 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
hasActiveSubscriptionis 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:
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:
- Server-side checks - Gate entire pages to subscribers
- Client-side checks - Conditionally render UI elements
- API route checks - Protect API endpoints
Let's start with server-side access control.
Was this helpful?