← Back to @payez/next-mvp
Tier System

Tier System

Free/Premium/Ultimate with configurable limits per feature.

Built-in plan tiers with per-feature limits, upgrade prompts, and enforcement. Define what each tier gets — the framework enforces it at the middleware level and shows upgrade CTAs when limits are hit.

How it works

1

Tiers are defined in your app configuration: name, Stripe price ID, and a map of feature limits (e.g., max projects: 3, max API calls/month: 10000, storage MB: 500).

2

Each user's current tier is stored in the database, updated automatically by the Stripe webhook when they subscribe, upgrade, downgrade, or cancel.

3

Middleware reads the user's tier on every request. API routes that check feature limits call a helper that compares current usage against the tier's limit for that feature.

4

When a user hits a limit, the response includes a structured error with the limit details and an upgrade URL. The built-in UI components show a contextual upgrade prompt.

Defining tiers

Tiers are configured in a single file. Each tier maps a feature key to a limit value. The framework reads this configuration at runtime — no code changes needed to adjust limits.

// lib/tiers.ts
export const tiers = {
  free: {
    name: 'Starter',
    stripePriceId: null,
    limits: {
      projects: 3,
      apiCallsPerMonth: 10_000,
      storageMb: 500,
      customDomains: 0,
      teamMembers: 1,
      webhooks: 2,
    }
  },
  premium: {
    name: 'Pro',
    stripePriceId: 'price_pro_monthly',
    limits: {
      projects: 25,
      apiCallsPerMonth: 100_000,
      storageMb: 5_000,
      customDomains: 3,
      teamMembers: 10,
      webhooks: 20,
    }
  },
  ultimate: {
    name: 'Business',
    stripePriceId: 'price_biz_monthly',
    limits: {
      projects: -1,   // -1 = unlimited
      apiCallsPerMonth: -1,
      storageMb: 50_000,
      customDomains: -1,
      teamMembers: -1,
      webhooks: -1,
    }
  }
};

Checking limits in your code

The MVP provides a checkLimit helper that compares the user's current usage against their tier's limit. It returns a structured result: allowed (boolean), current usage, limit, and tier name. Your code decides what to do — block the action, show an upgrade prompt, or proceed.

import { checkLimit } from '@payez/next-mvp';

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

  const result = await checkLimit(
    session.userId,
    'projects'  // feature key from tiers config
  );

  if (!result.allowed) {
    return Response.json({
      error: 'limit_reached',
      feature: 'projects',
      current: result.current,    // 3
      limit: result.limit,        // 3
      tier: result.tier,           // 'free'
      upgradeUrl: '/pricing',
    }, { status: 402 });
  }

  // Under the limit — proceed
  // ...create the project...
}

Automatic tier transitions

When a Stripe webhook fires (subscription created, updated, deleted), the MVP automatically resolves the Stripe price ID to a tier and updates the user's record. The next request from that user will see their new limits. Downgrades are handled gracefully: existing resources aren't deleted. The user just can't create new ones until they're under the new tier's limit. For example: a Pro user with 15 projects downgrades to Free (limit: 3). Their 15 projects still work — they just can't create a 16th.

Upgrade prompts in the UI

The MVP includes a TierGate component that wraps any UI element with a tier check. If the user is within their limit, the children render normally. If they've hit the limit, an upgrade prompt appears instead — with the specific feature, current usage, and a link to pricing.

// In your component
import { TierGate } from '@payez/next-mvp';

<TierGate feature="projects" action="create">
  {/* This only renders if user is under limit */}
  <button onClick={createProject}>
    New Project
  </button>
</TierGate>

// If over limit, TierGate renders:
// "You've used 3 of 3 projects on the Starter plan.
//  Upgrade to Pro for up to 25 projects."
// [Upgrade] button

Usage tracking

Feature usage is tracked automatically for resources managed by the MVP (projects, team members, etc.). For custom features (API calls, storage), you increment the counter via a helper. Usage counters reset on the billing cycle date (tracked from the Stripe subscription). Monthly limits like API calls reset automatically — you don't need a cron job.

This is included. Not extra.

Tier System ships with every @payez/next-mvp install. 10K MAU free. No credit card required.