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.
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).
Each user's current tier is stored in the database, updated automatically by the Stripe webhook when they subscribe, upgrade, downgrade, or cancel.
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.
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.
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,
}
}
};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...
}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.
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] buttonFeature 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.
Tier System ships with every @payez/next-mvp install. 10K MAU free. No credit card required.