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.
