Usage & Bonus System
Space usage tracking, plan limits, bonus credits, and redemption codes
Usage & Bonus System
ProductReady includes a comprehensive usage tracking and bonus system that allows you to:
- Track resource usage per space (AI Credits, Posts, Storage)
- Enforce plan limits (Free, Pro, Enterprise)
- Grant bonus credits via redemption codes or promotions
- Display usage in dashboard with progress bars and warnings
This system is separate from the payment billing system. It tracks what users consume, while billing handles how they pay.
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ Plan Configuration │
│ (src/config/plan-limits.ts + pricing-plans.ts) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Database Schema │
│ ┌──────────────┐ ┌─────────────────┐ ┌────────────────┐ │
│ │ spaceUsage │ │ spaceBonusUsage │ │ bonusTemplates │ │
│ │ (tracking) │ │ (bonus credits) │ │ (admin config) │ │
│ └──────────────┘ └─────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Service Layer │
│ ┌──────────────────┐ ┌─────────────────────────────────┐ │
│ │ usage-service.ts │ │ bonus-service.ts │ │
│ │ - getUsageSummary│ │ - grantBonusFromTemplate │ │
│ │ - consumeUsage │ │ - grantManualBonus │ │
│ └──────────────────┘ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ tRPC Routers │
│ usage.ts │ bonusTemplates.ts │ redemptionCodes.ts │
└─────────────────────────────────────────────────────────────┘Plan Configuration
Plan Limits
Define resource limits for each plan in src/config/plan-limits.ts:
// src/config/plan-limits.ts
export type UsageType = "ai_credits" | "posts" | "storage";
export type PeriodType = "monthly" | "total";
export interface UsageLimit {
limit: number; // -1 for unlimited
period: PeriodType;
label: string;
}
export interface FeatureFlag {
enabled: boolean;
label: string;
}
export interface PlanLimits {
aiCredits: UsageLimit;
posts: UsageLimit;
storage: UsageLimit;
openApi: FeatureFlag;
}
const MB = 1024 * 1024;
const GB = MB * 1024;
export const UNLIMITED = -1;
export const PLAN_LIMITS: Record<PlanKey, PlanLimits> = {
Free: {
aiCredits: { limit: 50, period: "monthly", label: "AI Credits" },
posts: { limit: 100, period: "total", label: "Posts" },
storage: { limit: 100 * MB, period: "total", label: "Storage" },
openApi: { enabled: false, label: "OpenAPI Access" },
},
Pro: {
aiCredits: { limit: 500, period: "monthly", label: "AI Credits" },
posts: { limit: 1000, period: "total", label: "Posts" },
storage: { limit: 10 * GB, period: "total", label: "Storage" },
openApi: { enabled: true, label: "OpenAPI Access" },
},
Enterprise: {
aiCredits: { limit: UNLIMITED, period: "monthly", label: "AI Credits" },
posts: { limit: UNLIMITED, period: "total", label: "Posts" },
storage: { limit: UNLIMITED, period: "total", label: "Storage" },
openApi: { enabled: true, label: "OpenAPI Access" },
},
};Pricing Plans
Define plan versions for billing in src/config/pricing-plans.ts:
// src/config/pricing-plans.ts
export interface PricingPlan {
id: string; // Stable ID (e.g., "pro_v1")
name: string; // Display name
plan: string; // Plan type (Free, Pro, Enterprise)
priceMonthly: number; // Monthly price in cents
priceYearly: number; // Yearly price in cents
isActive: boolean; // Whether this version is available
}
export const PRICING_PLANS: PricingPlan[] = [
{ id: "free_v1", name: "Free", plan: "Free", priceMonthly: 0, priceYearly: 0, isActive: true },
{ id: "pro_v1", name: "Pro", plan: "Pro", priceMonthly: 2900, priceYearly: 29000, isActive: true },
{ id: "enterprise_v1", name: "Enterprise", plan: "Enterprise", priceMonthly: 9900, priceYearly: 99000, isActive: true },
];Database Schema
Space Usage
Tracks current usage for each space:
// src/db/schema/space-usage.ts
export const spaceUsage = pgTable("space_usage", {
spaceId: text("space_id").primaryKey(),
aiCreditsUsed: integer("ai_credits_used").default(0).notNull(),
postsUsed: integer("posts_used").default(0).notNull(),
storageUsed: bigint("storage_used", { mode: "number" }).default(0).notNull(),
currentPeriod: text("current_period").notNull(), // "YYYY-MM"
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at"),
});Space Bonus Usage
Tracks bonus credits granted to spaces:
// src/db/schema/space-bonus-usage.ts
export const spaceBonusUsage = pgTable("space_bonus_usage", {
id: text("id").primaryKey(),
spaceId: text("space_id").notNull(),
type: text("type").notNull(), // ai_credits, posts, storage
amount: integer("amount").notNull(), // Total bonus amount
used: integer("used").default(0), // Amount consumed
source: text("source").notNull(), // redemption, promotion, manual
sourceName: text("source_name").notNull(),
sourceRef: text("source_ref"),
expiresAt: timestamp("expires_at"), // null = no expiry
createdAt: timestamp("created_at").defaultNow().notNull(),
});Bonus Templates
Admin-defined bonus packages:
// src/db/schema/space-bonus-templates.ts
export const spaceBonusTemplates = pgTable("space_bonus_templates", {
id: text("id").primaryKey(),
name: text("name").notNull(),
description: text("description"),
type: text("type").notNull(), // ai_credits, posts, storage
amount: integer("amount").notNull(),
durationDays: integer("duration_days"), // null = no expiry
applicablePlans: text("applicable_plans").array(),
isActive: boolean("is_active").default(true).notNull(),
createdBy: text("created_by"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});Usage Service
Get Usage Summary
import { getUsageSummary } from "~/lib/billing";
const summary = await getUsageSummary(spaceId);
// Returns:
// {
// spaceId: "space_123",
// plan: "Pro",
// planId: "pro_v1",
// resetDate: "2024-02-01",
// items: [
// { type: "ai_credits", used: 150, limit: 1100, percentage: 13.6, isWarning: false },
// { type: "posts", used: 25, limit: 100, percentage: 25, isWarning: false },
// ],
// features: [
// { type: "custom_domain", enabled: true, label: "Custom Domain" },
// ],
// }Consume Usage
import { consumeUsage } from "~/lib/billing";
const result = await consumeUsage(spaceId, "ai_credits", 10);
// Returns:
// { success: true, remaining: 90 }
// or
// { success: false, error: "quota_exhausted" }Consumption Priority
When consuming usage, the system follows this priority:
- Bonus credits first (earliest expiry first)
- Plan quota (when bonuses exhausted)
- Reject (when all quota exhausted)
Bonus Service
Grant Bonus from Template
import { grantBonusFromTemplate } from "~/lib/billing";
const result = await grantBonusFromTemplate({
spaceId: "space_123",
templateId: "tmpl_welcome_bonus",
sourceRef: "code_ABC123",
durationDaysOverride: 30, // Optional: override template duration
});
// Returns: { success: true, bonus: BonusItemVO }Grant Manual Bonus
import { grantManualBonus } from "~/lib/billing";
const result = await grantManualBonus({
spaceId: "space_123",
type: "ai_credits",
amount: 500,
sourceName: "Customer Support",
durationDays: 90,
});Redemption Codes
Redemption codes support polymorphic rewards:
Reward Types
| Type | Description |
|---|---|
plan | Upgrade space to a plan (Free/Pro/Enterprise) |
bonus | Grant bonus credits from a template |
Schema
export const redemptionCodes = pgTable("redemption_codes", {
id: text("id").primaryKey(),
code: text("code").notNull().unique(),
rewardType: text("reward_type").default("plan").notNull(), // "plan" or "bonus"
// Plan reward fields
planType: text("plan_type"), // Free, Pro, Enterprise
durationDays: integer("duration_days"), // null = lifetime
// Bonus reward fields
bonusTemplateId: text("bonus_template_id"),
// Common fields
maxRedemptions: integer("max_redemptions"),
currentRedemptions: integer("current_redemptions").default(0),
isActive: text("is_active").default("true"),
expiresAt: timestamp("expires_at"),
createdBy: text("created_by").notNull(),
});Creating Codes (Admin)
// Create a code that upgrades to Pro for 30 days
await trpc.redemptionCodes.create.mutate({
code: "PROMO2024",
rewardType: "plan",
planType: "Pro",
durationDays: 30,
maxRedemptions: 100,
description: "Launch promotion",
});// Create a code that grants bonus credits
await trpc.redemptionCodes.create.mutate({
code: "BONUS500",
rewardType: "bonus",
bonusTemplateId: "tmpl_500_credits",
durationDays: 60, // Override template duration
maxRedemptions: 50,
});Redeeming Codes (User)
const result = await trpc.redemptionCodes.redeem.mutate({
code: "PROMO2024",
spaceId: "space_123",
});
// Returns:
// {
// success: true,
// rewardType: "plan",
// planType: "Pro",
// expiresAt: "2024-02-01T00:00:00Z",
// bonusGranted: false,
// }Sharing Redemption Instructions with End Users
As a developer, you can use these templates to guide your users through the redemption process. Simply copy and customize the message below.
When distributing redemption codes to your users, you can use these copy-paste templates:
Template 1: Plan Upgrade Code
Hello [User Name],
Thank you for your interest in upgrading your workspace! Here's your exclusive redemption code:
🎁 CODE: [YOUR_CODE_HERE]
To redeem your code and upgrade to [Pro/Enterprise] plan:
1. Visit: https://yourapp.com/redeem
2. Sign in to your account (or create one if you're new)
3. Enter the redemption code: [YOUR_CODE_HERE]
4. Select the workspace you want to upgrade
5. Click "Redeem Code"
Your workspace will be immediately upgraded! This code is valid [for 30 days / until MM/DD/YYYY / lifetime].
If you have any questions, feel free to reach out to our support team.
Best regards,
[Your Team Name]Template 2: Bonus Credits Code
Hi [User Name],
Great news! We're giving you bonus credits to try out our premium features.
🎁 BONUS CODE: [YOUR_CODE_HERE]
How to claim your bonus:
1. Go to https://yourapp.com/redeem
2. Log in to your account
3. Enter your code: [YOUR_CODE_HERE]
4. Choose your workspace
5. Hit "Redeem Code"
You'll instantly receive [X] bonus AI credits / posts / storage that you can use for [duration].
Enjoy exploring the platform!
[Your Team Name]Template 3: Quick Email Format
Subject: Your Redemption Code is Ready! 🎁
Hi [User Name],
Your redemption code: [YOUR_CODE_HERE]
Redeem at: https://yourapp.com/redeem
This code will [upgrade your plan to Pro/grant you X bonus credits].
Questions? Reply to this email.
Thanks,
[Your Team]Redemption Page Features
Your users will see a user-friendly interface at /redeem that includes:
- Automatic code detection: If you share a link like
https://yourapp.com/redeem?code=PROMO2024, the code will be pre-filled - Sign-in protection: Users must be logged in to redeem codes
- Workspace selection: Users can choose which workspace to apply the code to
- Instant confirmation: Success message with details about the upgrade or bonus
- Current plan visibility: Users can see their current plan before redeeming
Best Practices for Distribution
- Use direct links: Share
https://yourapp.com/redeem?code=YOUR_CODEto pre-fill the code - Set expectations: Tell users what they'll receive (plan upgrade, credits amount, duration)
- Provide support: Include contact information for redemption issues
- Track redemptions: Monitor redemption rates in the admin panel at
/systemadmin/redemption-codes - Set expiration dates: Use time-limited codes to create urgency
tRPC API Reference
Usage Router
// Get usage summary for current space
trpc.usage.getSummary.query()
// Consume usage (internal use)
trpc.usage.consume.mutate({ type: "ai_credits", amount: 10 })Bonus Templates Router (Admin)
// List templates
trpc.bonusTemplates.list.query({ isActive: true })
// Create template
trpc.bonusTemplates.create.mutate({
name: "Welcome Bonus",
type: "ai_credits",
amount: 100,
durationDays: 30,
applicablePlans: ["Free", "Pro"],
})
// Update template
trpc.bonusTemplates.update.mutate({ id: "tmpl_123", isActive: false })
// Delete template
trpc.bonusTemplates.delete.mutate({ id: "tmpl_123" })Redemption Codes Router
// List codes (admin)
trpc.redemptionCodes.list.query({ rewardType: "bonus", isActive: "true" })
// Create code (admin)
trpc.redemptionCodes.create.mutate({ ... })
// Batch create codes (admin)
trpc.redemptionCodes.batchCreate.mutate({
prefix: "LAUNCH",
count: 50,
rewardType: "plan",
planType: "Pro",
durationDays: 30,
})
// Redeem code (user)
trpc.redemptionCodes.redeem.mutate({ code: "ABC123", spaceId: "space_123" })
// Get redemption history (admin)
trpc.redemptionCodes.getHistory.query({ codeId: "code_123" })UI Components
Usage Panel
Display usage in dashboard:
import { UsagePanel } from "~/components/dashboard/usage-panel";
<UsagePanel spaceId={spaceId} />Usage Progress Bar
Individual usage item:
import { UsageProgressBar } from "~/components/dashboard/usage-progress-bar";
<UsageProgressBar
label="AI Credits"
used={150}
limit={1000}
period="monthly"
isWarning={false}
/>Bonus Table
Display active bonuses:
import { BonusTable } from "~/components/dashboard/bonus-table";
<BonusTable spaceId={spaceId} />System Admin Pages
Bonus Templates Management
Navigate to: /systemadmin/bonus-templates
Features:
- Create/edit/delete bonus templates
- Set amount, duration, applicable plans
- Toggle active status
Redemption Codes Management
Navigate to: /systemadmin/redemption-codes
Features:
- Create single or batch codes
- Choose reward type (plan or bonus)
- View redemption history
- Export codes to CSV
Monthly Reset
Monthly usage (like AI Credits) automatically resets at the start of each billing period:
// In getUsageSummary()
const currentPeriod = getCurrentPeriod(); // "2024-01"
if (usage.currentPeriod !== currentPeriod) {
// Reset monthly counters
await resetMonthlyUsage(spaceId, currentPeriod);
}Best Practices
1. Check Usage Before Operations
async function generateContent(spaceId: string) {
const result = await consumeUsage(spaceId, "ai_credits", 1);
if (!result.success) {
throw new Error("AI credits exhausted. Please upgrade your plan.");
}
// Proceed with generation...
}2. Show Warnings at 80%
The isWarning flag is set when usage exceeds 80%:
{item.isWarning && (
<Alert variant="warning">
You've used {item.percentage}% of your {item.label}.
Consider upgrading your plan.
</Alert>
)}3. Grant Bonuses for Engagement
// Grant bonus when user completes onboarding
await grantPromotionalBonus(
spaceId,
"ai_credits",
50,
"Onboarding Completion",
30 // 30 days expiry
);Troubleshooting
Usage Not Updating
- Check
spaceUsagerecord exists for the space - Verify
currentPeriodmatches current month - Check for database transaction errors
Bonus Not Applied
- Verify template is active (
isActive: true) - Check plan is in
applicablePlansarray - Verify bonus hasn't expired
Redemption Code Fails
- Check code is active and not expired
- Verify max redemptions not reached
- Check space hasn't already redeemed this code