Paywall

Gate UI features and API routes by plan using requirePlan and the PlanGate component.

Overview

Nebutra provides two complementary mechanisms for enforcing plan-based access:

MechanismWhere to use
requirePlan(planId)Server-side: API route middleware, Server Components, Server Actions
<PlanGate require="plan">Client-side: React components, conditional UI rendering

Both read the current tenant's active plan from the database — no client-side state to spoof.

Server Component paywall

Use requirePlan in a Next.js Server Component to redirect users below the required plan:

import { requirePlan } from "@nebutra/billing";
import { getCurrentTenant } from "@nebutra/tenant";

export default async function AnalyticsPage() {
  const tenant = getCurrentTenant();

  // Redirects to /billing/upgrade if the tenant is not on PRO or higher
  await requirePlan("pro", tenant.tenantId);

  return <AnalyticsDashboard />;
}

requirePlan behavior

Tenant planRequired planResult
freeproRedirects to /billing/upgrade
proproPasses through
enterpriseproPasses through (enterprise ≥ pro)
proenterpriseRedirects to /billing/upgrade

Plan hierarchy is free < pro < enterprise. requirePlan("pro") allows both pro and enterprise tenants through.

API route gating

Protect API routes with requirePlan as middleware:

import { requirePlan } from "@nebutra/billing";
import { getCurrentTenant } from "@nebutra/tenant";

app.post("/api/v1/exports", async (c) => {
  const tenant = getCurrentTenant();

  // Returns 403 Forbidden with a structured error if plan is insufficient
  await requirePlan("pro", tenant.tenantId, { throwOnFail: true });

  // ... handle the export
});

When throwOnFail: true, requirePlan throws a structured error that the API gateway error handler converts to:

HTTP 403 Forbidden

{
  "success": false,
  "error": {
    "code": "PLAN_REQUIRED",
    "message": "This feature requires the PRO plan.",
    "details": {
      "requiredPlan": "pro",
      "currentPlan": "free",
      "upgradeUrl": "https://app.nebutra.com/billing/upgrade"
    }
  }
}

React <PlanGate> component

Use <PlanGate> to conditionally render UI based on the current tenant's plan. This is a Server Component — plan data is never exposed to the client.

import { PlanGate } from "@nebutra/billing/react";

// Hard block — shows an upgrade prompt when plan is insufficient
<PlanGate require="pro">
  <AdvancedAnalytics />
</PlanGate>

// Custom fallback — show your own upgrade UI
<PlanGate require="pro" fallback={<UpgradeBanner feature="Advanced Analytics" />}>
  <AdvancedAnalytics />
</PlanGate>

// Silent hide — renders nothing when plan is insufficient (no upgrade prompt)
<PlanGate require="pro" fallback={null}>
  <ExportButton />
</PlanGate>

Default upgrade prompt

When no fallback is provided, <PlanGate> renders the built-in upgrade prompt:

┌────────────────────────────────────────────────┐
│  🔒  This feature requires the PRO plan.        │
│                                                  │
│  [Upgrade to PRO →]                              │
└────────────────────────────────────────────────┘

The button links to /billing/upgrade.

Upgrade redirect target

The /billing/upgrade route creates a Stripe Checkout session and redirects the user. It is pre-built and requires no configuration beyond the environment variables.

If you want to customize the upgrade page (e.g., to show a plan comparison table before redirecting), edit:

apps/web/src/app/billing/upgrade/page.tsx

Showing upgrade prompts vs hard blocks

Use the following heuristics to choose between a soft prompt and a hard block:

ScenarioRecommendation
Feature is discoverable but gated (e.g., export button)Soft prompt — show <PlanGate> with upgrade CTA
Route only makes sense on the paid plan (e.g., SSO settings)Hard redirect via requirePlan in the Server Component
Sensitive data that must never reach FREE usersHard block on both Server Component and API route
Feature preview / teaserRender a disabled/locked version with tooltip via <PlanGate fallback={...}>

Never rely on client-side plan checks alone. Always enforce access on the server using requirePlan. A motivated user can bypass any client-side guard.

How is this guide?

Edit on GitHub

Last updated on

On this page