Plan-Based Rendering
Learn how to render content conditionally based on user's subscription plan and configure plan quotas
Indie Kit allows you to render different content based on the user's subscription plan. Let's explore how to implement plan-based rendering and configure plan quotas! 💫
Using useCurrentPlan Hook 🎨
The useCurrentPlan
hook provides information about the user's current subscription plan. Here's a complete example:
// src/app/(in-app)/app/page.tsx
'use client'
import { useCurrentPlan } from '@/lib/subscription/useCurrentPlan'
import { Button } from '@/components/ui/button'
import { CreditCardIcon } from 'lucide-react'
import Link from 'next/link'
function AppHomepage() {
const { currentPlan } = useCurrentPlan()
return (
<div className="flex flex-col gap-4">
<h1 className="text-2xl font-bold">Dashboard</h1>
{/* Display current plan status */}
<p className="text-sm text-muted-foreground">
{currentPlan
? `You are on the ${currentPlan.name} plan.`
: "You are not subscribed to any plan."}
</p>
{/* Conditional rendering based on plan */}
{currentPlan ? (
<Link href="/app/billing">
<Button>
<CreditCardIcon className="w-4 h-4" />
<span>Manage Subscription</span>
</Button>
</Link>
) : (
<Link href="/app/pricing">
<Button>
<CreditCardIcon className="w-4 h-4" />
<span>Subscribe</span>
</Button>
</Link>
)}
{/* Debug information (optional) */}
{currentPlan ? (
<pre>{JSON.stringify({ currentPlan }, null, 2)}</pre>
) : null}
</div>
)
}
Configuring Plan Quotas ⚙️
Define plan quotas using the quotaSchema
in your database schema:
// src/db/schema/plans.ts
import { z } from 'zod'
export const quotaSchema = z.object({
canUseApp: z.boolean().default(true),
numberOfThings: z.number(),
somethingElse: z.string(),
})
Using Quotas in Components 📊
Here's how to use quotas to control feature access:
function FeatureComponent() {
const { currentPlan } = useCurrentPlan()
// Check if user can access feature
if (!currentPlan?.quota.canUseApp) {
return (
<div className="p-4 bg-muted rounded-lg">
<p>Please upgrade your plan to access this feature</p>
<Link href="/app/pricing">
<Button>Upgrade Now</Button>
</Link>
</div>
)
}
// Check numerical limits
if (currentPlan.quota.numberOfThings <= 0) {
return (
<div className="p-4 bg-muted rounded-lg">
<p>You've reached your limit for this feature</p>
<Link href="/app/billing">
<Button>Increase Limit</Button>
</Link>
</div>
)
}
return (
<div>
{/* Your feature content */}
<p>Remaining uses: {currentPlan.quota.numberOfThings}</p>
</div>
)
}
Best Practices 💡
-
Graceful Degradation
- Always provide meaningful messages for restricted features
- Offer clear upgrade paths
- Handle edge cases (no plan, expired plan, etc.)
-
Performance
- Cache plan data when possible
- Avoid unnecessary re-renders
- Use loading states during plan checks
-
User Experience
- Show feature limitations before user actions
- Provide clear upgrade benefits
- Maintain consistent UI across plan levels
Common Patterns 🔄
Feature Gates
function PremiumFeature() {
const { currentPlan } = useCurrentPlan()
const isPremium = currentPlan?.name === 'premium'
if (!isPremium) {
return (
<div className="text-center p-6">
<h3 className="text-lg font-semibold">Premium Feature</h3>
<p className="text-muted-foreground">
Upgrade to Premium to access this feature
</p>
<Link href="/app/pricing">
<Button variant="outline" className="mt-4">
View Plans
</Button>
</Link>
</div>
)
}
return <div>{/* Premium feature content */}</div>
}
Usage Limits
function LimitedFeature() {
const { currentPlan } = useCurrentPlan()
const limit = currentPlan?.quota.numberOfThings ?? 0
const used = 5 // Get this from your usage tracking
return (
<div>
<div className="flex justify-between items-center">
<h3>Your Usage</h3>
<span>{used} / {limit}</span>
</div>
<progress
value={used}
max={limit}
className="w-full mt-2"
/>
</div>
)
}
Server-Side Plan Checks 🔒
API Routes with withAuthRequired
The withAuthRequired
middleware provides a powerful context object that includes session data and plan information:
// src/app/api/app/feature/route.ts
import withAuthRequired from '@/lib/auth/withAuthRequired'
import { NextResponse } from 'next/server'
export const GET = withAuthRequired(async (req, context) => {
// Access session data
const { session } = context
// Get current plan using the provided helper
const currentPlan = await context.getCurrentPlan()
// Check plan quotas
if (!currentPlan?.quotas.canUseApp) {
return NextResponse.json(
{ error: 'Feature not available in your plan' },
{ status: 403 }
)
}
// Check numerical limits
if (currentPlan.quotas.numberOfThings <= 0) {
return NextResponse.json(
{ error: 'You have reached your usage limit' },
{ status: 403 }
)
}
// Your API logic here
return NextResponse.json({
data: 'Your feature data',
remainingQuota: currentPlan.quotas.numberOfThings
})
})
export const POST = withAuthRequired(async (req, context) => {
const { session } = context
const currentPlan = await context.getCurrentPlan()
const data = await req.json()
// Access user information
const userId = session.user.id
const userEmail = session.user.email
// Plan-based logic
if (currentPlan?.codename !== 'premium') {
return NextResponse.json(
{ error: 'This action requires a premium plan' },
{ status: 403 }
)
}
// Process the request
return NextResponse.json({ success: true })
})
Context Features 📋
The withAuthRequired
middleware provides:
session
: Authenticated user session datagetCurrentPlan()
: Helper function to fetch current planparams
: Route parameters (if any)
Server Components
For server components, you can still use the auth helper:
import { auth } from '@/auth'
import { getUserPlan } from '@/lib/plans/getUserPlan'
export default async function ProtectedFeature() {
const session = await auth()
const plan = await getUserPlan(session.user.id)
if (!plan?.quota.canUseApp) {
return <div>Feature not available in your plan</div>
}
return <div>{/* Protected content */}</div>
}
Now you can implement plan-based rendering and quota management in your Indie Kit application! Remember to always provide a great user experience regardless of the user's plan level. 🚀