Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component...
npx skills add flowglad/skills --skill "flowglad-feature-gating"
Install specific skill from multi-skill repository
# Description
Implement feature access checks using Flowglad to gate premium features, create paywalls, and restrict functionality based on subscription status. Use this skill when adding paid-only features or checking user entitlements.
# SKILL.md
name: flowglad-feature-gating
description: Implement feature access checks using Flowglad to gate premium features, create paywalls, and restrict functionality based on subscription status. Use this skill when adding paid-only features or checking user entitlements.
license: MIT
metadata:
author: flowglad
version: "1.0.0"
Feature Gating
Abstract
Implement feature access checks using Flowglad's checkFeatureAccess method to gate premium features, create paywalls, and restrict functionality based on subscription status.
Table of Contents
- Loading State Handling — CRITICAL
- 1.1 Wait for Billing to Load
- 1.2 Skeleton Loading Patterns
- Server-Side Gating — HIGH
- 2.1 Verify Access on Server
- 2.2 API Route Protection
- Feature Identification — MEDIUM
- 3.1 Use Slugs Not IDs
- Component Wrapper Patterns — MEDIUM
- 4.1 Feature Gate Component
- 4.2 Higher-Order Component Pattern
- Redirect to Upgrade Patterns — MEDIUM
- 5.1 Client-Side Redirect
- 5.2 Server-Side Redirect
1. Loading State Handling
Impact: CRITICAL
The billing hook loads asynchronously. While loading, checkFeatureAccess is null (not a function). If you try to call it before loading completes, you'll get a runtime error or incorrect behavior. This causes premium users to see upgrade prompts or paywalls incorrectly.
Note: The
flowglad()factory function used in server-side examples must be set up in your project (typically at@/lib/flowglad). See the setup skill for configuration instructions.
1.1 Wait for Billing to Load
Impact: CRITICAL (prevents flash of incorrect content)
Users with active subscriptions will see upgrade prompts flash briefly if you don't wait for billing to load before checking access.
Incorrect: checks access before billing loads
function PremiumFeature() {
const { checkFeatureAccess } = useBilling()
// BUG: checkFeatureAccess is null while loading!
// This will throw: "checkFeatureAccess is not a function"
if (!checkFeatureAccess('premium-feature')) {
return <UpgradePrompt />
}
return <PremiumContent />
}
This crashes because checkFeatureAccess is null until billing data loads, not a callable function.
Correct: check both loaded and checkFeatureAccess
function PremiumFeature() {
const { loaded, checkFeatureAccess } = useBilling()
if (!loaded || !checkFeatureAccess) {
return <LoadingSkeleton />
}
if (!checkFeatureAccess('premium-feature')) {
return <UpgradePrompt />
}
return <PremiumContent />
}
Always check both loaded and checkFeatureAccess before calling the function to ensure billing data is available.
1.2 Skeleton Loading Patterns
Impact: CRITICAL (prevents layout shift)
Show appropriate loading states that match the expected content dimensions to prevent layout shift.
Incorrect: shows nothing or spinner
function Dashboard() {
const { loaded, checkFeatureAccess } = useBilling()
if (!loaded) {
return null // Content disappears!
}
return <DashboardContent />
}
Correct: show skeleton matching content layout
function Dashboard() {
const { loaded, checkFeatureAccess } = useBilling()
if (!loaded || !checkFeatureAccess) {
return (
<div className="space-y-4">
<div className="h-8 w-48 bg-gray-200 animate-pulse rounded" />
<div className="h-64 bg-gray-200 animate-pulse rounded" />
</div>
)
}
return <DashboardContent />
}
2. Server-Side Gating
Impact: HIGH
Client-side feature checks are for UI purposes only. Any sensitive operation or data access must verify subscription status server-side. Users can bypass client-side checks by modifying frontend code or using browser developer tools.
2.1 Verify Access on Server
Impact: HIGH (security requirement)
Never trust client-side access checks for operations that cost money, access sensitive data, or perform privileged actions.
Incorrect: trusts client-side check for sensitive operation
// API route
export async function POST(req: Request) {
// Client could bypass this by modifying frontend code
const { hasAccess } = await req.json()
if (!hasAccess) {
return Response.json({ error: 'No access' }, { status: 403 })
}
return performSensitiveOperation()
}
Correct: verify server-side
// API route
import { flowglad } from '@/lib/flowglad'
import { auth } from '@/lib/auth'
export async function POST(req: Request) {
const session = await auth()
if (!session?.user?.id) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const billing = await flowglad(session.user.id).getBilling()
if (!billing.checkFeatureAccess('api-access')) {
return Response.json({ error: 'Upgrade required' }, { status: 403 })
}
return performSensitiveOperation()
}
2.2 API Route Protection
Impact: HIGH (prevents unauthorized access)
Create a reusable pattern for protecting multiple API routes with feature checks.
Incorrect: duplicates check logic everywhere
// routes/generate.ts
export async function POST(req: Request) {
const session = await auth()
const billing = await flowglad(session.user.id).getBilling()
if (!billing.checkFeatureAccess('ai-generation')) {
return Response.json({ error: 'Upgrade required' }, { status: 403 })
}
// ... generation logic
}
// routes/export.ts
export async function POST(req: Request) {
const session = await auth()
const billing = await flowglad(session.user.id).getBilling()
if (!billing.checkFeatureAccess('export')) {
return Response.json({ error: 'Upgrade required' }, { status: 403 })
}
// ... export logic
}
Correct: create reusable middleware/helper
// lib/requireFeature.ts
import { flowglad } from '@/lib/flowglad'
import { auth } from '@/lib/auth'
export async function requireFeature(featureSlug: string) {
const session = await auth()
if (!session?.user?.id) {
return { error: 'Unauthorized', status: 401 }
}
const billing = await flowglad(session.user.id).getBilling()
if (!billing.checkFeatureAccess(featureSlug)) {
return { error: 'Upgrade required', status: 403 }
}
return { userId: session.user.id, billing }
}
// routes/generate.ts
export async function POST(req: Request) {
const result = await requireFeature('ai-generation')
if ('error' in result) {
return Response.json({ error: result.error }, { status: result.status })
}
const { userId, billing } = result
// ... generation logic
}
3. Feature Identification
Impact: MEDIUM
How you reference features affects code maintainability and environment portability.
3.1 Use Slugs Not IDs
Impact: MEDIUM (environment portability)
Feature IDs are auto-generated and differ between development, staging, and production environments. Slugs are stable identifiers you control.
Incorrect: hardcoding Flowglad IDs
// IDs change between environments!
if (billing.checkFeatureAccess('feat_abc123xyz')) {
// Works in dev, breaks in production
}
Correct: use slugs
// Slugs are stable across environments
if (billing.checkFeatureAccess('advanced-analytics')) {
// Works everywhere
}
Define feature slugs in your Flowglad dashboard and reference them consistently in code.
4. Component Wrapper Patterns
Impact: MEDIUM
Reusable patterns for gating components reduce boilerplate and ensure consistent behavior.
4.1 Feature Gate Component
Impact: MEDIUM (reduces boilerplate)
Create a declarative component for gating content.
Incorrect: repeats gate logic in every component
function AnalyticsDashboard() {
const { loaded, checkFeatureAccess } = useBilling()
if (!loaded || !checkFeatureAccess) return <Skeleton />
if (!checkFeatureAccess('analytics')) return <UpgradePrompt feature="analytics" />
return <Analytics />
}
function ExportButton() {
const { loaded, checkFeatureAccess } = useBilling()
if (!loaded || !checkFeatureAccess) return <Skeleton />
if (!checkFeatureAccess('export')) return <UpgradePrompt feature="export" />
return <ExportUI />
}
Correct: create reusable FeatureGate component
// components/FeatureGate.tsx
import { useBilling } from '@flowglad/nextjs'
import { ReactNode } from 'react'
interface FeatureGateProps {
feature: string
children: ReactNode
fallback?: ReactNode
loading?: ReactNode
}
export function FeatureGate({
feature,
children,
fallback = <UpgradePrompt />,
loading = <Skeleton />,
}: FeatureGateProps) {
const { loaded, checkFeatureAccess } = useBilling()
if (!loaded || !checkFeatureAccess) {
return <>{loading}</>
}
if (!checkFeatureAccess(feature)) {
return <>{fallback}</>
}
return <>{children}</>
}
// Usage
function AnalyticsDashboard() {
return (
<FeatureGate feature="analytics">
<Analytics />
</FeatureGate>
)
}
function ExportButton() {
return (
<FeatureGate feature="export" fallback={<LockedExportButton />}>
<ExportUI />
</FeatureGate>
)
}
4.2 Higher-Order Component Pattern
Impact: MEDIUM (alternative pattern for class components or full-page gates)
Use HOC pattern when you need to gate entire pages or components.
Incorrect: duplicates page-level checks
// pages/analytics.tsx
export default function AnalyticsPage() {
const { loaded, checkFeatureAccess } = useBilling()
if (!loaded || !checkFeatureAccess) return <PageSkeleton />
if (!checkFeatureAccess('analytics')) {
// Using redirect() in a client component - this won't work!
redirect('/pricing')
return null
}
return <AnalyticsDashboard />
}
Correct: create withFeatureAccess HOC
'use client'
// lib/withFeatureAccess.tsx
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useBilling } from '@flowglad/nextjs'
import { ComponentType } from 'react'
export function withFeatureAccess<P extends object>(
WrappedComponent: ComponentType<P>,
feature: string,
redirectTo = '/pricing'
) {
return function WithFeatureAccess(props: P) {
const { loaded, checkFeatureAccess } = useBilling()
const router = useRouter()
useEffect(() => {
if (loaded && checkFeatureAccess && !checkFeatureAccess(feature)) {
router.push(redirectTo)
}
}, [loaded, checkFeatureAccess, router])
if (!loaded || !checkFeatureAccess) {
return <PageSkeleton />
}
if (!checkFeatureAccess(feature)) {
// Show skeleton while redirecting
return <PageSkeleton />
}
return <WrappedComponent {...props} />
}
}
// Usage
function AnalyticsDashboard() {
return <div>Analytics content</div>
}
export default withFeatureAccess(AnalyticsDashboard, 'analytics')
Note: For better UX without flash, prefer server-side gating (see Section 5.2) when possible.
5. Redirect to Upgrade Patterns
Impact: MEDIUM
When users lack access, redirect them to upgrade rather than showing error states.
5.1 Client-Side Redirect
Impact: MEDIUM (better UX than error states)
Redirect users to pricing/upgrade page when they try to access gated features.
Incorrect: shows error message
function PremiumPage() {
const { loaded, checkFeatureAccess } = useBilling()
if (!loaded || !checkFeatureAccess) return <Skeleton />
if (!checkFeatureAccess('premium')) {
return <div>Error: You don't have access to this feature</div>
}
return <PremiumContent />
}
Correct: redirect to upgrade with context
'use client'
import { useEffect } from 'react'
import { useRouter, usePathname } from 'next/navigation'
import { useBilling } from '@flowglad/nextjs'
function PremiumPage() {
const { loaded, checkFeatureAccess } = useBilling()
const router = useRouter()
const pathname = usePathname()
useEffect(() => {
if (loaded && checkFeatureAccess && !checkFeatureAccess('premium')) {
// Redirect with return URL so user comes back after upgrade
router.push(`/pricing?upgrade=premium&returnTo=${encodeURIComponent(pathname)}`)
}
}, [loaded, checkFeatureAccess, router, pathname])
if (!loaded || !checkFeatureAccess || !checkFeatureAccess('premium')) {
return <Skeleton />
}
return <PremiumContent />
}
5.2 Server-Side Redirect
Impact: MEDIUM (prevents page flash)
For server components or middleware, check access server-side before rendering.
Incorrect: client-side check causes flash
// Page loads, then redirects - user sees flash
export default function PremiumPage() {
return (
<ClientSideGate feature="premium">
<PremiumContent />
</ClientSideGate>
)
}
Correct: check in server component or middleware
// app/premium/page.tsx (Server Component)
import { redirect } from 'next/navigation'
import { auth } from '@/lib/auth'
import { flowglad } from '@/lib/flowglad'
export default async function PremiumPage() {
const session = await auth()
if (!session?.user?.id) {
redirect('/login')
}
const billing = await flowglad(session.user.id).getBilling()
if (!billing.checkFeatureAccess('premium')) {
redirect('/pricing?upgrade=premium')
}
return <PremiumContent />
}
Or using middleware for multiple routes:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const PREMIUM_ROUTES = ['/analytics', '/export', '/api-access']
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Check if this is a premium route
if (PREMIUM_ROUTES.some((route) => pathname.startsWith(route))) {
// Note: Full billing check requires server-side call
// For middleware, you might check a session flag or JWT claim
// set during login that indicates subscription tier
const session = await getSession(request)
if (!session?.isPremium) {
return NextResponse.redirect(
new URL(`/pricing?returnTo=${pathname}`, request.url)
)
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/analytics/:path*', '/export/:path*', '/api-access/:path*'],
}
Note: Full Flowglad billing checks in middleware require additional setup. For most cases, server component checks (pattern above) are simpler and recommended.
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.