Indie Kit DocsIndie Kit Docs
Tutorials

Plan-Based Rendering 🎯

Learn how to render content conditionally based on user's subscription plan and configure plan quotas

Plan-Based Rendering 🎯

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="/#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="/#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 💡

  1. Graceful Degradation

    • Always provide meaningful messages for restricted features
    • Offer clear upgrade paths
    • Handle edge cases (no plan, expired plan, etc.)
  2. Performance

    • Cache plan data when possible
    • Avoid unnecessary re-renders
    • Use loading states during plan checks
  3. 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="/#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 data
  • getCurrentPlan(): Helper function to fetch current plan
  • params: 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. 🚀

On this page