Skip to content

add-subscription-plan

Access: /template add-subscription-plan or /t add-subscription-plan

Adds new subscription tiers and pricing plans to your MakerKit project's billing system, including feature gating and plan limits.

What It Does

The add-subscription-plan command helps you:

  • Create new subscription tiers in Stripe
  • Define plan features and limits
  • Implement feature gating based on plans
  • Add upgrade/downgrade flows
  • Update pricing display components

Usage

bash
/template add-subscription-plan "Description"
# or
/t add-subscription-plan "Description"

When prompted, provide:

  • Plan name and description
  • Pricing (monthly and yearly)
  • Feature list and limits
  • Position in pricing hierarchy
  • Trial period configuration

Prerequisites

  • MakerKit project with Stripe configured
  • Existing billing infrastructure
  • Commercial MakerKit license from MakerKit

What Gets Created

1. Stripe Products and Prices

Creates products in Stripe:

typescript
// scripts/create-subscription-plan.ts
import Stripe from 'stripe';

export async function createSubscriptionPlan({
  name,
  description,
  monthlyPrice,
  yearlyPrice,
  features,
  limits,
}: PlanConfig) {
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
  
  // Create product
  const product = await stripe.products.create({
    name,
    description,
    metadata: {
      features: JSON.stringify(features),
      limits: JSON.stringify(limits),
    },
  });
  
  // Create monthly price
  const monthlyPriceObj = await stripe.prices.create({
    product: product.id,
    unit_amount: monthlyPrice * 100, // Convert to cents
    currency: 'usd',
    recurring: { interval: 'month' },
    nickname: `${name} Monthly`,
    metadata: {
      tier: name.toLowerCase(),
      period: 'monthly',
    },
  });
  
  // Create yearly price (with discount)
  const yearlyPriceObj = await stripe.prices.create({
    product: product.id,
    unit_amount: yearlyPrice * 100,
    currency: 'usd',
    recurring: { interval: 'year' },
    nickname: `${name} Yearly`,
    metadata: {
      tier: name.toLowerCase(),
      period: 'yearly',
    },
  });
  
  return {
    productId: product.id,
    monthlyPriceId: monthlyPriceObj.id,
    yearlyPriceId: yearlyPriceObj.id,
  };
}

2. Plan Configuration

Update billing configuration:

typescript
// config/billing.config.ts
export const BILLING_PLANS = {
  free: {
    name: 'Free',
    description: 'Get started for free',
    price: { monthly: 0, yearly: 0 },
    stripeIds: {
      monthly: null,
      yearly: null,
    },
    features: [
      '1 team member',
      '1GB storage',
      'Community support',
      'Basic features',
    ],
    limits: {
      teamMembers: 1,
      storage: 1_000_000_000, // 1GB in bytes
      projects: 3,
      apiCalls: 1000,
    },
  },
  
  starter: {
    name: 'Starter',
    description: 'Perfect for small teams',
    price: { monthly: 19, yearly: 190 },
    stripeIds: {
      monthly: process.env.STRIPE_PRICE_STARTER_MONTHLY,
      yearly: process.env.STRIPE_PRICE_STARTER_YEARLY,
    },
    features: [
      'Up to 5 team members',
      '10GB storage',
      'Email support',
      'Advanced features',
      'API access',
    ],
    limits: {
      teamMembers: 5,
      storage: 10_000_000_000, // 10GB
      projects: 10,
      apiCalls: 10000,
    },
  },
  
  professional: {
    name: 'Professional',
    description: 'For growing businesses',
    price: { monthly: 49, yearly: 490 },
    stripeIds: {
      monthly: process.env.STRIPE_PRICE_PRO_MONTHLY,
      yearly: process.env.STRIPE_PRICE_PRO_YEARLY,
    },
    features: [
      'Up to 20 team members',
      '100GB storage',
      'Priority support',
      'All features',
      'Unlimited API calls',
      'Custom integrations',
      'Advanced analytics',
    ],
    limits: {
      teamMembers: 20,
      storage: 100_000_000_000, // 100GB
      projects: 50,
      apiCalls: -1, // Unlimited
    },
    popular: true,
  },
  
  enterprise: {
    name: 'Enterprise',
    description: 'For large organizations',
    price: { monthly: 'custom', yearly: 'custom' },
    stripeIds: {
      monthly: null,
      yearly: null,
    },
    features: [
      'Unlimited team members',
      'Unlimited storage',
      'Dedicated support',
      'All features',
      'Unlimited everything',
      'SLA guarantee',
      'Custom development',
      'On-premise option',
    ],
    limits: {
      teamMembers: -1,
      storage: -1,
      projects: -1,
      apiCalls: -1,
    },
    customPricing: true,
  },
};

3. Feature Gating Implementation

Check plan limits:

typescript
// lib/billing/feature-gates.ts
import { BILLING_PLANS } from '@/config/billing.config';

export async function checkPlanLimit(
  accountId: string,
  limitType: keyof typeof BILLING_PLANS.free.limits
): Promise<{ allowed: boolean; limit: number; current: number }> {
  // Get current subscription
  const subscription = await getActiveSubscription(accountId);
  const plan = subscription?.planId || 'free';
  const planConfig = BILLING_PLANS[plan];
  
  if (!planConfig) {
    throw new Error('Invalid plan');
  }
  
  const limit = planConfig.limits[limitType];
  
  // Unlimited check
  if (limit === -1) {
    return { allowed: true, limit: -1, current: 0 };
  }
  
  // Get current usage
  const current = await getCurrentUsage(accountId, limitType);
  
  return {
    allowed: current < limit,
    limit,
    current,
  };
}

// Usage in server actions
export const createProjectAction = enhanceAction(
  async (data, user) => {
    // Check plan limits
    const { allowed, limit, current } = await checkPlanLimit(
      data.accountId,
      'projects'
    );
    
    if (!allowed) {
      throw new Error(
        `Project limit reached (${current}/${limit}). Please upgrade your plan.`
      );
    }
    
    // Create project
    return createProject(data);
  },
  {
    auth: true,
    schema: CreateProjectSchema,
  }
);

4. Plan Comparison Component

Display plan differences:

typescript
// components/billing/plan-comparison.tsx
export function PlanComparison({ currentPlan }: { currentPlan?: string }) {
  const plans = Object.values(BILLING_PLANS);
  
  // Extract all unique features
  const allFeatures = Array.from(
    new Set(plans.flatMap(plan => plan.features))
  );
  
  return (
    <div className="overflow-x-auto">
      <table className="w-full">
        <thead>
          <tr>
            <th className="text-left p-4">Feature</th>
            {plans.map(plan => (
              <th key={plan.name} className="text-center p-4">
                <div>
                  {plan.name}
                  {plan.name.toLowerCase() === currentPlan && (
                    <Badge className="ml-2">Current</Badge>
                  )}
                </div>
                <div className="text-sm font-normal text-muted-foreground">
                  {plan.customPricing ? (
                    'Contact us'
                  ) : (
                    `$${plan.price.monthly}/mo`
                  )}
                </div>
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {allFeatures.map(feature => (
            <tr key={feature} className="border-t">
              <td className="p-4">{feature}</td>
              {plans.map(plan => (
                <td key={plan.name} className="text-center p-4">
                  {plan.features.includes(feature) ? (
                    <Check className="h-5 w-5 text-green-500 mx-auto" />
                  ) : (
                    <X className="h-5 w-5 text-gray-300 mx-auto" />
                  )}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

5. Upgrade Prompt Component

Encourage plan upgrades:

typescript
// components/billing/upgrade-prompt.tsx
export function UpgradePrompt({ 
  feature,
  currentLimit,
  requiredPlan,
}: {
  feature: string;
  currentLimit?: number;
  requiredPlan?: string;
}) {
  const router = useRouter();
  const { account } = useAccount();
  
  return (
    <Card className="border-warning">
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <AlertCircle className="h-5 w-5 text-warning" />
          Upgrade Required
        </CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        <p>
          {currentLimit ? (
            <>You've reached the limit of {currentLimit} {feature}.</>
          ) : (
            <>This feature requires the {requiredPlan} plan or higher.</>
          )}
        </p>
        
        <div className="flex gap-2">
          <Button
            onClick={() => router.push(`/home/${account.slug}/billing`)}
          >
            View Plans
          </Button>
          <Button variant="outline">
            Contact Sales
          </Button>
        </div>
      </CardContent>
    </Card>
  );
}

6. Usage Tracking

Monitor plan usage:

typescript
// lib/billing/usage-tracking.ts
export async function trackUsage(
  accountId: string,
  metric: string,
  amount: number = 1
) {
  const client = getSupabaseServerClient();
  
  // Upsert usage record
  await client
    .from('usage_metrics')
    .upsert({
      account_id: accountId,
      metric,
      period: new Date().toISOString().slice(0, 7), // YYYY-MM
      usage: amount,
    }, {
      onConflict: 'account_id,metric,period',
      update: {
        usage: client.raw('usage + ?', [amount]),
      },
    });
}

// Usage dashboard
export function UsageDashboard({ accountId }: { accountId: string }) {
  const { data: usage } = useUsageMetrics(accountId);
  const { data: subscription } = useSubscription(accountId);
  const plan = BILLING_PLANS[subscription?.planId || 'free'];
  
  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
      {Object.entries(plan.limits).map(([metric, limit]) => {
        const current = usage?.[metric] || 0;
        const percentage = limit === -1 ? 0 : (current / limit) * 100;
        
        return (
          <Card key={metric}>
            <CardHeader>
              <CardTitle className="text-sm font-medium">
                {formatMetricName(metric)}
              </CardTitle>
            </CardHeader>
            <CardContent>
              <div className="text-2xl font-bold">
                {current}
                {limit !== -1 && (
                  <span className="text-sm font-normal text-muted-foreground">
                    {' '}/ {limit}
                  </span>
                )}
              </div>
              {limit !== -1 && (
                <Progress value={percentage} className="mt-2" />
              )}
            </CardContent>
          </Card>
        );
      })}
    </div>
  );
}

Plan Migration

Handle plan changes:

typescript
// lib/billing/plan-migration.ts
export async function migratePlan(
  accountId: string,
  fromPlan: string,
  toPlan: string
) {
  // Check if downgrade is allowed
  if (isDowngrade(fromPlan, toPlan)) {
    const canDowngrade = await checkDowngradeEligibility(
      accountId,
      fromPlan,
      toPlan
    );
    
    if (!canDowngrade.eligible) {
      throw new Error(
        `Cannot downgrade: ${canDowngrade.reason}`
      );
    }
  }
  
  // Apply any necessary data migrations
  await applyPlanMigrations(accountId, fromPlan, toPlan);
  
  // Update subscription in Stripe
  await updateStripeSubscription(accountId, toPlan);
}

Testing Plans

typescript
describe('Subscription Plans', () => {
  it('enforces plan limits', async () => {
    const account = await createTestAccount({ plan: 'free' });
    
    // Create up to limit
    for (let i = 0; i < 3; i++) {
      await createProject({ accountId: account.id });
    }
    
    // Should fail on limit
    await expect(
      createProject({ accountId: account.id })
    ).rejects.toThrow('Project limit reached');
  });
  
  it('allows unlimited for enterprise', async () => {
    const account = await createTestAccount({ plan: 'enterprise' });
    
    // Should allow many
    for (let i = 0; i < 100; i++) {
      await createProject({ accountId: account.id });
    }
  });
});

License Requirement

Important: This command requires a commercial MakerKit license from https://makerkit.dev?atp=MqaGgc MakerKit is a premium SaaS starter kit and requires proper licensing for commercial use.

Built with ❤️ for the AI Coding community, by Praney Behl