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 💡

  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="/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 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. 🚀