fix(billing): increase free tier credits (#862)

* Update free tier to 10

* Lint
This commit is contained in:
Siddharth Ganesan
2025-08-04 12:34:21 -07:00
committed by GitHub
parent 58e764c1dd
commit e71a736400
13 changed files with 52 additions and 34 deletions

View File

@@ -12,6 +12,7 @@ import {
Skeleton,
} from '@/components/ui'
import { useSession, useSubscription } from '@/lib/auth-client'
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
import { createLogger } from '@/lib/logs/console/logger'
import {
BillingSummary,
@@ -227,7 +228,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
subscription.isEnterprise ||
(subscription.isTeam && isTeamAdmin)
}
minimumLimit={usageLimitData?.minimumLimit ?? 5}
minimumLimit={usageLimitData?.minimumLimit ?? DEFAULT_FREE_CREDITS}
/>
)}
</div>

View File

@@ -16,6 +16,7 @@ import {
uuid,
vector,
} from 'drizzle-orm/pg-core'
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
import { TAG_SLOTS } from '@/lib/constants/knowledge'
// Custom tsvector type for full-text search
@@ -451,7 +452,9 @@ export const userStats = pgTable('user_stats', {
totalChatExecutions: integer('total_chat_executions').notNull().default(0),
totalTokensUsed: integer('total_tokens_used').notNull().default(0),
totalCost: decimal('total_cost').notNull().default('0'),
currentUsageLimit: decimal('current_usage_limit').notNull().default('5'), // Default $5 for free plan
currentUsageLimit: decimal('current_usage_limit')
.notNull()
.default(DEFAULT_FREE_CREDITS.toString()), // Default $10 for free plan
usageLimitSetBy: text('usage_limit_set_by'), // User ID who set the limit (for team admin tracking)
usageLimitUpdatedAt: timestamp('usage_limit_updated_at').defaultNow(),
// Billing period tracking

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react'
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
import type { SubscriptionFeatures } from '@/lib/billing/types'
import { createLogger } from '@/lib/logs/console/logger'
@@ -89,7 +90,7 @@ export function useSubscriptionState() {
usage: {
current: data?.usage?.current ?? 0,
limit: data?.usage?.limit ?? 5,
limit: data?.usage?.limit ?? DEFAULT_FREE_CREDITS,
percentUsed: data?.usage?.percentUsed ?? 0,
isWarning: data?.usage?.isWarning ?? false,
isExceeded: data?.usage?.isExceeded ?? false,
@@ -214,9 +215,9 @@ export function useUsageLimit() {
}
return {
currentLimit: data?.currentLimit ?? 5,
currentLimit: data?.currentLimit ?? DEFAULT_FREE_CREDITS,
canEdit: data?.canEdit ?? false,
minimumLimit: data?.minimumLimit ?? 5,
minimumLimit: data?.minimumLimit ?? DEFAULT_FREE_CREDITS,
plan: data?.plan ?? 'free',
setBy: data?.setBy,
updatedAt: data?.updatedAt ? new Date(data.updatedAt) : null,

View File

@@ -20,6 +20,7 @@ import {
renderPasswordResetEmail,
} from '@/components/emails/render-email'
import { getBaseURL } from '@/lib/auth-client'
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
import { env, isTruthy } from '@/lib/env'
import { isProd } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
@@ -1080,7 +1081,7 @@ export const auth = betterAuth({
name: 'free',
priceId: env.STRIPE_FREE_PRICE_ID || '',
limits: {
cost: env.FREE_TIER_COST_LIMIT ?? 5,
cost: env.FREE_TIER_COST_LIMIT ?? DEFAULT_FREE_CREDITS,
sharingEnabled: 0,
multiplayerEnabled: 0,
workspaceCollaborationEnabled: 0,

View File

@@ -2,6 +2,11 @@
* Billing and cost constants shared between client and server code
*/
/**
* Default free credits (in dollars) for new users
*/
export const DEFAULT_FREE_CREDITS = 10
/**
* Base charge applied to every workflow execution
* This charge is applied regardless of whether the workflow uses AI models

View File

@@ -1,4 +1,5 @@
import { and, eq } from 'drizzle-orm'
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
import {
resetOrganizationBillingPeriod,
resetUserBillingPeriod,
@@ -917,7 +918,7 @@ function getDefaultBillingSummary(type: 'individual' | 'organization') {
currentUsage: 0,
overageAmount: 0,
totalProjected: 0,
usageLimit: 5,
usageLimit: DEFAULT_FREE_CREDITS,
percentUsed: 0,
isWarning: false,
isExceeded: false,
@@ -935,7 +936,7 @@ function getDefaultBillingSummary(type: 'individual' | 'organization') {
// Usage details
usage: {
current: 0,
limit: 5,
limit: DEFAULT_FREE_CREDITS,
percentUsed: 0,
isWarning: false,
isExceeded: false,

View File

@@ -1,4 +1,5 @@
import { and, eq } from 'drizzle-orm'
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
import { getPlanPricing } from '@/lib/billing/core/billing'
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
import { createLogger } from '@/lib/logs/console/logger'
@@ -87,7 +88,7 @@ export async function getOrganizationBillingData(
// Process member data
const members: MemberUsageData[] = membersWithUsage.map((memberRecord) => {
const currentUsage = Number(memberRecord.currentPeriodCost || 0)
const usageLimit = Number(memberRecord.currentUsageLimit || 5)
const usageLimit = Number(memberRecord.currentUsageLimit || DEFAULT_FREE_CREDITS)
const percentUsed = usageLimit > 0 ? (currentUsage / usageLimit) * 100 : 0
return {
@@ -197,13 +198,14 @@ export async function updateMemberUsageLimit(
// Validate minimum limit based on plan
const planLimits = {
free: 5,
free: DEFAULT_FREE_CREDITS,
pro: 20,
team: 40,
enterprise: 100, // Default, can be overridden by metadata
}
let minimumLimit = planLimits[subscription.plan as keyof typeof planLimits] || 5
let minimumLimit =
planLimits[subscription.plan as keyof typeof planLimits] || DEFAULT_FREE_CREDITS
// For enterprise, check metadata for custom limits
if (subscription.plan === 'enterprise' && subscription.metadata) {

View File

@@ -1,5 +1,6 @@
import { and, eq, inArray } from 'drizzle-orm'
import { client } from '@/lib/auth-client'
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
import {
calculateDefaultUsageLimit,
checkEnterprisePlan,
@@ -156,7 +157,7 @@ export async function hasExceededCostLimit(userId: string): Promise<boolean> {
const subscription = await getHighestPrioritySubscription(userId)
// Calculate usage limit
let limit = 5 // Default free tier limit
let limit = DEFAULT_FREE_CREDITS // Default free tier limit
if (subscription) {
limit = calculateDefaultUsageLimit(subscription)
logger.info('Using subscription-based limit', {
@@ -338,7 +339,7 @@ export async function getUserSubscriptionState(userId: string): Promise<UserSubs
// Check cost limit using already-fetched user stats
let hasExceededLimit = false
if (isProd && statsRecords.length > 0) {
let limit = 5 // Default free tier limit
let limit = DEFAULT_FREE_CREDITS // Default free tier limit
if (subscription) {
limit = calculateDefaultUsageLimit(subscription)
}

View File

@@ -1,4 +1,5 @@
import { and, eq } from 'drizzle-orm'
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
import { calculateDefaultUsageLimit, canEditUsageLimit } from '@/lib/billing/subscriptions/utils'
import type { BillingData, UsageData, UsageLimitInfo } from '@/lib/billing/types'
@@ -29,7 +30,7 @@ export async function getUserUsageData(userId: string): Promise<UsageData> {
await initializeUserUsageLimit(userId)
return {
currentUsage: 0,
limit: 5,
limit: DEFAULT_FREE_CREDITS,
percentUsed: 0,
isWarning: false,
isExceeded: false,
@@ -98,7 +99,7 @@ export async function getUserUsageLimitInfo(userId: string): Promise<UsageLimitI
let minimumLimit: number
if (!subscription || subscription.status !== 'active') {
// Free plan users
minimumLimit = 5
minimumLimit = DEFAULT_FREE_CREDITS
} else if (subscription.plan === 'pro') {
// Pro plan users: $20 minimum
minimumLimit = 20
@@ -131,9 +132,9 @@ export async function getUserUsageLimitInfo(userId: string): Promise<UsageLimitI
if (userStatsRecord.length === 0) {
await initializeUserUsageLimit(userId)
return {
currentLimit: 5,
currentLimit: DEFAULT_FREE_CREDITS,
canEdit: false,
minimumLimit: 5,
minimumLimit: DEFAULT_FREE_CREDITS,
plan: 'free',
setBy: null,
updatedAt: null,
@@ -171,16 +172,16 @@ export async function initializeUserUsageLimit(userId: string): Promise<void> {
return // User already has usage stats, don't override
}
// Create initial usage stats with default $5 limit
// Create initial usage stats with default free credits limit
await db.insert(userStats).values({
id: crypto.randomUUID(),
userId,
currentUsageLimit: '5', // Default $5 for new users
currentUsageLimit: DEFAULT_FREE_CREDITS.toString(), // Default free credits for new users
usageLimitUpdatedAt: new Date(),
billingPeriodStart: new Date(), // Start billing period immediately
})
logger.info('Initialized usage limit for new user', { userId, limit: 5 })
logger.info('Initialized usage limit for new user', { userId, limit: DEFAULT_FREE_CREDITS })
} catch (error) {
logger.error('Failed to initialize usage limit', { userId, error })
throw error
@@ -233,7 +234,7 @@ export async function updateUserUsageLimit(
if (!subscription || subscription.status !== 'active') {
// Free plan users (shouldn't reach here due to canEditUsageLimit check above)
minimumLimit = 5
minimumLimit = DEFAULT_FREE_CREDITS
} else if (subscription.plan === 'pro') {
// Pro plan users: $20 minimum
minimumLimit = 20
@@ -311,7 +312,7 @@ export async function getUserUsageLimit(userId: string): Promise<number> {
if (userStatsQuery.length === 0) {
// User doesn't have stats yet, initialize and return default
await initializeUserUsageLimit(userId)
return 5 // Default free plan limit
return DEFAULT_FREE_CREDITS // Default free plan limit
}
return Number.parseFloat(userStatsQuery[0].currentUsageLimit)
@@ -380,16 +381,16 @@ export async function syncUsageLimitsFromSubscription(userId: string): Promise<v
// Only update if subscription is free plan or if current limit is below new minimum
if (!subscription || subscription.status !== 'active') {
// User downgraded to free plan - cap at $5
// User downgraded to free plan - cap at default free credits
await db
.update(userStats)
.set({
currentUsageLimit: '5',
currentUsageLimit: DEFAULT_FREE_CREDITS.toString(),
usageLimitUpdatedAt: new Date(),
})
.where(eq(userStats.userId, userId))
logger.info('Synced usage limit to free plan', { userId, limit: 5 })
logger.info('Synced usage limit to free plan', { userId, limit: DEFAULT_FREE_CREDITS })
} else if (currentLimit < defaultLimit) {
// User upgraded and current limit is below new minimum - raise to minimum
await db
@@ -451,7 +452,7 @@ export async function getTeamUsageLimits(organizationId: string): Promise<
userId: memberData.userId,
userName: memberData.userName,
userEmail: memberData.userEmail,
currentLimit: Number.parseFloat(memberData.currentLimit || '5'),
currentLimit: Number.parseFloat(memberData.currentLimit || DEFAULT_FREE_CREDITS.toString()),
currentUsage: Number.parseFloat(memberData.currentPeriodCost || '0'),
totalCost: Number.parseFloat(memberData.totalCost || '0'),
lastActive: memberData.lastActive,

View File

@@ -3,7 +3,7 @@ import { calculateDefaultUsageLimit, checkEnterprisePlan } from '@/lib/billing/s
vi.mock('@/lib/env', () => ({
env: {
FREE_TIER_COST_LIMIT: 5,
FREE_TIER_COST_LIMIT: 10,
PRO_TIER_COST_LIMIT: 20,
TEAM_TIER_COST_LIMIT: 40,
ENTERPRISE_TIER_COST_LIMIT: 200,
@@ -27,15 +27,15 @@ describe('Subscription Utilities', () => {
describe('calculateDefaultUsageLimit', () => {
it.concurrent('returns free-tier limit when subscription is null', () => {
expect(calculateDefaultUsageLimit(null)).toBe(5)
expect(calculateDefaultUsageLimit(null)).toBe(10)
})
it.concurrent('returns free-tier limit when subscription is undefined', () => {
expect(calculateDefaultUsageLimit(undefined)).toBe(5)
expect(calculateDefaultUsageLimit(undefined)).toBe(10)
})
it.concurrent('returns free-tier limit when subscription is not active', () => {
expect(calculateDefaultUsageLimit({ plan: 'pro', status: 'canceled', seats: 1 })).toBe(5)
expect(calculateDefaultUsageLimit({ plan: 'pro', status: 'canceled', seats: 1 })).toBe(10)
})
it.concurrent('returns pro limit for active pro plan', () => {

View File

@@ -1,3 +1,4 @@
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
import { env } from '@/lib/env'
export function checkEnterprisePlan(subscription: any): boolean {
@@ -20,7 +21,7 @@ export function checkTeamPlan(subscription: any): boolean {
*/
export function calculateDefaultUsageLimit(subscription: any): number {
if (!subscription || subscription.status !== 'active') {
return env.FREE_TIER_COST_LIMIT || 0
return env.FREE_TIER_COST_LIMIT || DEFAULT_FREE_CREDITS
}
const seats = subscription.seats || 1
@@ -45,7 +46,7 @@ export function calculateDefaultUsageLimit(subscription: any): number {
return seats * (env.ENTERPRISE_TIER_COST_LIMIT || 0)
}
return env.FREE_TIER_COST_LIMIT || 0
return env.FREE_TIER_COST_LIMIT || DEFAULT_FREE_CREDITS
}
/**

View File

@@ -56,7 +56,7 @@ async function runBillingTestSuite(): Promise<TestResults> {
logger.info('\n📋 Creating test users...')
// Free user (no overage billing)
const freeUser = await createTestUser('free', 5) // $5 usage on free plan
const freeUser = await createTestUser('free', 10) // $10 usage on free plan
results.users.push(freeUser)
// Pro user with no overage

View File

@@ -1,5 +1,6 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
import { createLogger } from '@/lib/logs/console/logger'
import type {
BillingStatus,
@@ -22,7 +23,7 @@ const defaultFeatures: SubscriptionFeatures = {
const defaultUsage: UsageData = {
current: 0,
limit: 5,
limit: DEFAULT_FREE_CREDITS,
percentUsed: 0,
isWarning: false,
isExceeded: false,