// Expert knowledge on Stripe integration, subscription plans (Glow Up, Viral Surge, Fame Flex), trial logic, plan enforcement, webhooks, and billing synchronization. Use this skill when user asks about "subscription", "billing", "stripe", "payment", "plan limits", "trial", "upgrade", "downgrade", "webhook", or "plan enforcement".
| name | billing-system-expert |
| description | Expert knowledge on Stripe integration, subscription plans (Glow Up, Viral Surge, Fame Flex), trial logic, plan enforcement, webhooks, and billing synchronization. Use this skill when user asks about "subscription", "billing", "stripe", "payment", "plan limits", "trial", "upgrade", "downgrade", "webhook", or "plan enforcement". |
| allowed-tools | Read, Grep, Glob, Bash |
You are an expert in the billing and subscription system for this influencer discovery platform. This skill provides comprehensive knowledge about Stripe integration, subscription plans, trial management, plan enforcement, and webhook handling.
This skill activates when users:
The platform offers three paid tiers plus a free tier:
Plan Structure:
// From /lib/db/schema.ts - subscription_plans table
{
planKey: 'glow_up' | 'viral_surge' | 'fame_flex' | 'free',
campaignsLimit: number, // -1 = unlimited
creatorsLimit: number, // -1 = unlimited
features: jsonb,
priceMonthly: number,
priceYearly: number
}
Plan Limits (from /lib/services/plan-enforcement.ts):
Glow Up (Entry Level)
process.env.STRIPE_GLOW_UP_MONTHLY_PRICE_IDprocess.env.STRIPE_GLOW_UP_YEARLY_PRICE_IDViral Surge (Pro Level)
process.env.STRIPE_VIRAL_SURGE_MONTHLY_PRICE_IDprocess.env.STRIPE_VIRAL_SURGE_YEARLY_PRICE_IDFame Flex (Unlimited)
process.env.STRIPE_FAME_FLEX_MONTHLY_PRICE_IDprocess.env.STRIPE_FAME_FLEX_YEARLY_PRICE_IDFree Tier (Default)
Service: /lib/services/plan-enforcement.ts
Key Functions:
class PlanEnforcementService {
// Get user's plan limits
static async getPlanLimits(userId: string): Promise<PlanLimits | null>
// Get current usage
static async getCurrentUsage(userId: string): Promise<UsageInfo | null>
// Validate campaign creation
static async validateCampaignCreation(userId: string): Promise<{
allowed: boolean;
reason?: string;
usage?: UsageInfo;
}>
// Validate job creation (creator searches)
static async validateJobCreation(userId: string, expectedCreators: number): Promise<{
allowed: boolean;
reason?: string;
usage?: UsageInfo;
adjustedLimit?: number;
}>
// Track campaign creation
static async trackCampaignCreated(userId: string): Promise<void>
// Track creators found
static async trackCreatorsFound(userId: string, creatorCount: number): Promise<void>
}
Usage Tracking:
Example Enforcement:
// Before creating campaign
const validation = await PlanEnforcementService.validateCampaignCreation(userId);
if (!validation.allowed) {
return NextResponse.json(
{ error: validation.reason, usage: validation.usage },
{ status: 403 }
);
}
// Create campaign...
// Track usage
await PlanEnforcementService.trackCampaignCreated(userId);
Dev Bypass (Non-Production Only):
// Environment variable bypass
PLAN_VALIDATION_BYPASS=all // or "campaigns,creators"
// Request header bypass
headers: {
'x-plan-bypass': 'all' // or "campaigns,creators"
}
Stripe Service: /lib/stripe/stripe-service.ts
Webhook Handler: /app/api/stripe/webhook/route.ts
Key Webhook Events:
checkout.session.completed
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
customer.subscription.trial_will_end
invoice.payment_succeeded
invoice.payment_failed
setup_intent.succeeded
payment_method.attached
Critical Logic (from webhook handler):
function getPlanFromPriceId(priceId: string): string {
const priceIdToplan = {
[process.env.STRIPE_GLOW_UP_MONTHLY_PRICE_ID!]: 'glow_up',
[process.env.STRIPE_GLOW_UP_YEARLY_PRICE_ID!]: 'glow_up',
[process.env.STRIPE_VIRAL_SURGE_MONTHLY_PRICE_ID!]: 'viral_surge',
[process.env.STRIPE_VIRAL_SURGE_YEARLY_PRICE_ID!]: 'viral_surge',
[process.env.STRIPE_FAME_FLEX_MONTHLY_PRICE_ID!]: 'fame_flex',
[process.env.STRIPE_FAME_FLEX_YEARLY_PRICE_ID!]: 'fame_flex',
};
return priceIdToplan[priceId] || 'unknown';
}
CRITICAL: Never use arbitrary fallback plans. If plan cannot be determined, throw error and retry webhook.
Trial Logic: /lib/services/trial-status-calculator.ts
Trial States:
inactive: No trial startedactive: Currently in trial periodexpired: Trial ended without conversionconverted: Trial converted to paid subscriptionTrial Activation:
// During subscription creation webhook
if (subscription.trial_end && subscription.status === 'trialing') {
await updateUserProfile(userId, {
trialStatus: 'active',
trialStartDate: new Date(),
trialEndDate: new Date(subscription.trial_end * 1000),
onboardingStep: 'completed'
});
}
Trial Conversion:
// During subscription update webhook
if (subscription.status === 'active' && user.trialStatus === 'active') {
await updateUserProfile(userId, {
trialStatus: 'converted',
trialConversionDate: new Date()
});
}
Field: billingSyncStatus in user_profiles table
Possible Values:
webhook_subscription_created - Subscription created successfullywebhook_subscription_updated - Subscription updatedwebhook_subscription_deleted - Subscription cancelledwebhook_trial_will_end - Trial ending soonwebhook_payment_succeeded - Payment successfulwebhook_payment_failed - Payment failedwebhook_setup_intent_succeeded - Payment method addedwebhook_payment_method_attached - Card attachedwebhook_emergency_fallback - Webhook failed, used fallbackChecking Sync Status:
node scripts/inspect-user-state.js --email user@example.com
// Good: Always validate before expensive operations
export async function POST(req: Request) {
const { userId } = await getAuthOrTest();
// Validate BEFORE creating campaign
const validation = await PlanEnforcementService.validateCampaignCreation(userId);
if (!validation.allowed) {
return NextResponse.json(
{
error: validation.reason,
usage: validation.usage,
upgrade_required: true
},
{ status: 403 }
);
}
// Create campaign...
const campaign = await db.insert(campaigns).values({ /* ... */ });
// Track usage AFTER success
await PlanEnforcementService.trackCampaignCreated(userId);
return NextResponse.json({ campaign });
}
When to use: Before any action that counts against limits
// Good: Always verify webhook signatures in production
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get('stripe-signature');
if (!signature) {
return NextResponse.json({ error: 'No signature' }, { status: 400 });
}
// Validate signature using Stripe SDK
const event = StripeService.validateWebhookSignature(body, signature);
// Process webhook event...
switch (event.type) {
case 'customer.subscription.created':
await handleSubscriptionCreated(event.data.object);
break;
// ...
}
return NextResponse.json({ received: true });
}
When to use: All Stripe webhook endpoints
// Good: Multiple fallback strategies for plan resolution
async function resolvePlanFromSubscription(subscription: Stripe.Subscription): Promise<string> {
// Strategy 1: Check metadata
let planId = subscription.metadata.plan || subscription.metadata.planId;
// Strategy 2: Derive from price ID
if (!planId || planId === 'unknown') {
const priceId = subscription.items.data[0]?.price?.id;
if (priceId) {
planId = getPlanFromPriceId(priceId);
}
}
// Strategy 3: Throw error and retry webhook
if (!planId || planId === 'unknown') {
throw new Error(
`Cannot determine plan for subscription ${subscription.id}. Will retry.`
);
}
return planId;
}
When to use: Processing subscription webhooks
// BAD: Can cause upgrade bugs where users get wrong plan
function getPlanFromPriceId(priceId: string): string {
const mapping = { /* ... */ };
return mapping[priceId] || 'glow_up'; // WRONG!
}
Why it's bad: User pays for Fame Flex but gets Glow Up limits
Do this instead:
// GOOD: Throw error and retry webhook
function getPlanFromPriceId(priceId: string): string {
const mapping = { /* ... */ };
const plan = mapping[priceId];
if (!plan) {
throw new Error(`Unknown price ID: ${priceId}. Webhook will retry.`);
}
return plan;
}
// BAD: User exceeds limit but usage is tracked anyway
await PlanEnforcementService.trackCampaignCreated(userId);
const validation = await PlanEnforcementService.validateCampaignCreation(userId);
if (!validation.allowed) {
return NextResponse.json({ error: 'Limit exceeded' }, { status: 403 });
}
Why it's bad: Usage counter increases even when action fails
Do this instead:
// GOOD: Validate โ Action โ Track
const validation = await PlanEnforcementService.validateCampaignCreation(userId);
if (!validation.allowed) {
return NextResponse.json({ error: 'Limit exceeded' }, { status: 403 });
}
const campaign = await createCampaign(/* ... */);
await PlanEnforcementService.trackCampaignCreated(userId);
// BAD: Accepting unauthenticated webhooks
export async function POST(req: Request) {
const event = await req.json();
// Process without verification - DANGEROUS!
await handleSubscriptionCreated(event.data.object);
}
Why it's bad: Anyone can forge webhooks and manipulate plans
Do this instead:
// GOOD: Always verify signatures
const body = await req.text();
const signature = req.headers.get('stripe-signature');
if (!signature) {
return NextResponse.json({ error: 'No signature' }, { status: 400 });
}
const event = StripeService.validateWebhookSignature(body, signature);
Symptoms:
Diagnosis:
billing_sync_status in database# Check user state
node scripts/inspect-user-state.js --email user@example.com
# Check webhook logs (if available)
grep "STRIPE-WEBHOOK" logs/app.log | grep "ERROR"
Solution:
# Manual sync (use admin endpoint or script)
curl -X POST http://localhost:3000/api/billing/sync-stripe \
-H "x-dev-auth: dev-bypass" \
-H "Content-Type: application/json" \
-d '{"userId": "user_xxx"}'
Symptoms:
Diagnosis:
PLAN_VALIDATION_BYPASS is not set in productionsubscription_plans tableSolution:
// Add enforcement to endpoint
import { PlanEnforcementService } from '@/lib/services/plan-enforcement';
export async function POST(req: Request) {
const { userId } = await getAuthOrTest();
// ADD THIS
const validation = await PlanEnforcementService.validateCampaignCreation(userId);
if (!validation.allowed) {
return NextResponse.json({ error: validation.reason }, { status: 403 });
}
// Create campaign...
// ADD THIS
await PlanEnforcementService.trackCampaignCreated(userId);
return NextResponse.json({ success: true });
}
Symptoms:
trial_status is inactiveonboarding_step not completedDiagnosis:
checkout.session.completed webhook firedtrial_end timestampfinalizeOnboarding was calledSolution:
# Manually complete onboarding
node scripts/complete-onboarding-and-activate-plan.js user_xxx
Or trigger via API:
curl -X POST http://localhost:3000/api/onboarding/complete \
-H "x-dev-auth: dev-bypass" \
-H "x-dev-user-id: user_xxx"
Symptoms:
Diagnosis:
.env has all STRIPE_*_PRICE_ID variablesSolution:
# Verify environment variables
grep "STRIPE_.*PRICE_ID" .env.local
# Expected output:
STRIPE_GLOW_UP_MONTHLY_PRICE_ID=price_xxx
STRIPE_GLOW_UP_YEARLY_PRICE_ID=price_yyy
# ... etc
If missing, add to .env.local and restart server.
Symptoms:
current_plan is correct but plan_campaigns_limit is wrongDiagnosis:
subscription.updated webhook firedsubscription_plans tableplanCampaignsLimit and planCreatorsLimitSolution:
// In webhook handler, ensure limits are updated:
const planDetails = await db.query.subscriptionPlans.findFirst({
where: eq(subscriptionPlans.planKey, planId)
});
await updateUserProfile(userId, {
currentPlan: planId,
planCampaignsLimit: planDetails?.campaignsLimit || 0,
planCreatorsLimit: planDetails?.creatorsLimit || 0
});
/lib/services/plan-enforcement.ts - Plan validation and usage tracking/lib/services/billing-service.ts - Billing operations/lib/stripe/stripe-service.ts - Stripe client wrapper/app/api/stripe/webhook/route.ts - Webhook event handlers/app/api/billing/status/route.ts - Get billing status/app/api/billing/sync-stripe/route.ts - Manual sync endpoint/app/api/campaigns/can-create/route.ts - Campaign validation endpoint/scripts/inspect-user-state.js - Diagnostic script/scripts/fix-user-billing-state.js - Fix scriptTest Plan Enforcement:
# Create user with specific plan
node scripts/complete-onboarding-and-activate-plan.js user_xxx glow_up
# Try creating campaigns
curl -X POST http://localhost:3000/api/campaigns \
-H "x-dev-user-id: user_xxx" \
-d '{"name": "Test Campaign 1"}'
# Check usage
curl http://localhost:3000/api/billing/status \
-H "x-dev-user-id: user_xxx"
Test Stripe Webhooks Locally:
# Install Stripe CLI
stripe listen --forward-to localhost:3000/api/stripe/webhook
# Trigger test webhook
stripe trigger customer.subscription.created
Expected Behavior:
User Checkout
โ
Stripe Checkout Session
โ
checkout.session.completed (webhook)
โ
Link Stripe Customer to User
โ
customer.subscription.created (webhook)
โ
Resolve Plan from Price ID
โ
Update user_profiles:
- current_plan
- plan_campaigns_limit
- plan_creators_limit
- stripe_subscription_id
- subscription_status
- trial_status (if trial)
โ
Finalize Onboarding
โ
User Can Access Platform
/docs/upgrade-user.md (if exists)