Feature Gating

Corral provides a full set of gating primitives for UI components, pages, API routes, and server functions. All gates read from the same session โ€” no extra API calls needed.

Generated gating components (v0.4.1)
gates.tsx includes AuthGate, PlanGate, FeatureGate, BlurGate, PageGate, and SkeletonPage. All gates support admin bypass by default.
Gate modes are set in corral.yaml
The visual behavior (blur, skeleton, block, hide) is configured per-feature in your YAML. Components respect this automatically โ€” you don't pass the mode as a prop. This lets you change gate behavior without touching React code.

FeatureGate

The primary gating component. Wraps content and shows or hides it based on whether the current user has access to a named feature.

Basic usage

tsx
import { FeatureGate } from '@corral/next'  // or @corral/react

// Gate mode comes from corral.yaml (ai_analysis has gate: blur)
<FeatureGate feature="ai_analysis">
  <AIAnalysisPanel />
</FeatureGate>

Override gate mode inline

tsx
// Override the YAML gate mode for this instance
<FeatureGate feature="ai_analysis" gate="block">
  <AIAnalysisPanel />
</FeatureGate>

// Custom fallback instead of the auto-generated upgrade prompt
<FeatureGate
  feature="ai_analysis"
  fallback={<UpgradeCard feature="ai_analysis" />}
>
  <AIAnalysisPanel />
</FeatureGate>

Props

PropTypeDescription
featurestringFeature name from corral.yaml
gate'blur'|'skeleton'|'block'|'hide'|'disabled'Override YAML gate mode
fallbackReactNodeCustom content to show when gated
sessionCorralSessionPass session explicitly (Server Components)
upgradeUrlstringOverride the upgrade CTA URL

PlanGate

Gate by plan level rather than by feature name. Useful for generic upgrade prompts or plan-specific UI elements.

tsx
import { PlanGate } from '@corral/next'

// Show for pro and above
<PlanGate plan="pro">
  <RealtimeFeed />
</PlanGate>

// Show ONLY for free users (inverse gate)
<PlanGate plan="free" exact>
  <FreeTrialBanner />
</PlanGate>

// Show for enterprise only
<PlanGate plan="enterprise" exact fallback={<ContactSalesCard />}>
  <AirGapConfig />
</PlanGate>

AuthGate

Show content only to authenticated (or unauthenticated) users. The simplest gate โ€” no plan or feature check involved.

tsx
import { AuthGate } from '@corral/next'

// Only for signed-in users
<AuthGate fallback={<SignInButton />}>
  <UserMenu />
</AuthGate>

// Only for signed-OUT users (show login prompt)
<AuthGate require="unauthenticated">
  <HeroCTA />
</AuthGate>

// Role check
<AuthGate role="admin">
  <AdminToolbar />
</AuthGate>

PageGate

Wraps an entire page. On the server, checks session and either renders the page or returns a gate response (redirect, blurred preview, or block). Use this in Next.js layouts or page components.

src/app/reports/advanced/page.tsxtsx
import { PageGate } from '@corral/next'

export default async function AdvancedReportsPage() {
  return (
    <PageGate
      feature="ai_analysis"
      gate="blur"
      upgradeUrl="/pricing"
      previewLines={8}  // show first 8 lines blurred
    >
      <AdvancedReportsContent />
    </PageGate>
  )
}

PageGate gate modes in detail

ModeServer behaviorClient behavior
redirect Server-side redirect to /login or custom URL N/A
block Renders a full-page upgrade prompt instead of content CSS-only blur overlay with upgrade CTA
blur Renders page with partial content, rest blurred CSS blur filter + overlay on blurred section
skeleton Renders page shell with skeleton loaders Animated skeleton with "upgrade to see data" hint

useFeatureGate

React hook for client-side conditional logic. Returns the gate state for a feature based on the current session.

tsx
import { useFeatureGate } from '@corral/react'

function ExportButton() {
  const { allowed, loading, plan } = useFeatureGate('bulk_export')

  if (loading) return <Skeleton width=120 />

  if (!allowed) {
    return (
      <button disabled title={`Upgrade to Enterprise to export bulk data`>
        Export All ๐Ÿ”’
      </button>
    )
  }

  return <button onClick={handleBulkExport}>Export All</button>
}

// useSession for full session object
import { useSession } from '@corral/react'

function PlanBadge() {
  const { session } = useSession()
  return <span>Plan: {session?.plan.id ?? 'anonymous'}</span>
}

Return value

FieldTypeDescription
allowedbooleanWhether the current user can access this feature
loadingbooleanSession still loading
reason'unauthenticated'|'wrong_plan'|'meter_exceeded'|'ok'Why access was denied
planstringThe minimum plan required
upgradeUrlstringGenerated upgrade URL for this feature

Server-Side Gating

Use requireFeature() or hasFeature() in server components, server actions, and API route handlers.

requireFeature โ€” throws on failure

typescript
import { requireFeature, getSession } from '@/lib/corral'

// In a server action:
'use server'
export async function runAnalysis(input: AnalysisInput) {
  await requireFeature('ai_analysis')
  // throws CorralGateError if not allowed
  // the error is caught by Next.js error boundaries
  return doAnalysis(input)
}

hasFeature โ€” check without throwing

typescript
import { getSession } from '@/lib/corral'

export async function GET(req: Request) {
  const session = await getSession()

  if (!session) {
    return Response.json({ error: 'unauthenticated' }, { status: 401 })
  }

  const canUseAI = session.features.includes('ai_analysis')
  // or: corral.hasFeature(session, 'ai_analysis')

  return Response.json({
    data: canUseAI ? await getFullData() : await getBasicData(),
    upgraded: canUseAI,
  })
}

API Route Gating

Use the withFeature wrapper for clean API route gating. Returns 403 with a structured error if the user doesn't have access.

src/app/api/analysis/route.tstypescript
import { withFeature } from '@corral/next'

export const POST = withFeature('ai_analysis', async (req, { session }) => {
  // Only reached if user has ai_analysis feature
  const body = await req.json()

  // Track usage meter
  await trackUsage(session.user.id, 'api_calls', 1)

  const result = await runAIAnalysis(body)
  return Response.json(result)
})

// Error response when gated:
// { "error": "feature_not_available",
//   "feature": "ai_analysis",
//   "required_plan": "pro",
//   "upgrade_url": "/pricing" }

Middleware Gating

Gate entire route groups in Next.js middleware without any React involved. Runs at the edge before the page loads.

middleware.tstypescript
import { corralMiddleware } from '@corral/next'

export function middleware(req) {
  return corralMiddleware(req, {
    // Must be authenticated
    protectedRoutes: ['/dashboard/:path*'],

    // Must have specific plan
    planRoutes: {
      '/reports/advanced/:path*': 'pro',
    },

    // Must have specific feature
    featureRoutes: {
      '/api/analysis/:path*': 'ai_analysis',
    },

    // Must have role
    roleRoutes: {
      '/admin/:path*': 'admin',
    },

    loginUrl: '/login',
    upgradeUrl: '/pricing',
  })
}

All 15 Gating Patterns

Every combination of subject, scope, and behavior you'll need:

01
Auth redirect
Redirect unauthenticated users to login. Server-side via middleware.
02
Auth block
Show login prompt inline instead of content. <AuthGate>
03
Feature hide
Completely remove a UI element for users without the feature.
04
Feature blur
Show blurred preview with upgrade CTA. Best for premium content.
05
Feature skeleton
Show loading skeleton. Best for charts and real-time data.
06
Feature block
Full-width upgrade prompt. High-friction, high-conversion.
07
Feature disabled
Button/control visible but disabled with tooltip. gate: disabled
08
Plan gate (min)
Show content only at or above a certain plan level. <PlanGate plan="pro">
09
Plan gate (exact)
Show content only for a specific plan. <PlanGate plan="free" exact>
10
Role gate
Show content only to users with a specific role. <AuthGate role="admin">
11
Page gate (server)
Block entire page server-side. Zero layout shift. <PageGate>
12
API route gate
Return 403 from API routes. withFeature('x', handler)
13
Meter gate
Block action when usage limit exceeded. checkLimit(userId, 'meter')
14
Trial gate
Show content only during trial period. session.plan.status === 'trialing'
15
Conditional render
Use useFeatureGate() hook for custom conditional logic in components.

Combining gates

Gates compose naturally. Use multiple gates in sequence for complex access rules:

tsx
{/* Must be logged in AND on pro+ AND not have exceeded meter */}
<AuthGate fallback={<LoginPrompt />}>
  <FeatureGate feature="ai_analysis">
    <MeterGate meter="api_calls" fallback={<LimitReachedBanner />}>
      <AIAnalysisPanel />
    </MeterGate>
  </FeatureGate>
</AuthGate>