Skip to main content

Plan Limits

Each subscription plan defines 17 feature limits that control how many resources a company (or user) can create. Limits are enforced at the GraphQL resolver level via the @CheckPlanLimit guard.

Limit Semantics

ValueMeaning
-1Unlimited (no restriction)
0Feature disabled (blocked entirely)
>0Specific numeric cap
tip

Enterprise plans (billingMode: POSTPAID) have all 16 limits set to -1 (unlimited). Both the backend PlanLimitGuard and the background-tasks BillingGate bypass plan-limit checks for POSTPAID subscriptions. Even if a POSTPAID plan accidentally carried a finite limit in DB, the guard's explicit POSTPAID early-return prevents rate-limiting. See Enterprise Post-Paid Billing.

All 17 Feature Limits

FeatureTypeWhat It CountsProtected Mutation
maxCompaniesTotalCompanies where user is billing ownercreateCompany (user-level check)
maxJobsTotalJobs per companycreateJob
maxTalentPoolsTotalTalent pools per companycreateTalentPool
maxContactsTotalContacts per companycreateContact
maxPipelinesTotalPipelines per companycreatePipeline
maxCvCollectionsTotalCV collections per companycreateCvCollection
maxSharedTalentListsTotalShared talent lists per companycreateSharedTalentList
maxCollaborationInvitesTotalCollaboration invitations per companycreateCollaborationInvitation
maxUsersPerCompanyTotalTeam members per companyinviteToCompany
maxContactNotesTotalContact notes per companycreateContactNote
maxInterviewsPerMonthMonthlyInterviews this calendar monthcreateInterview
maxMeetingsPerMonthMonthlyMeetings this calendar monthcreateMeeting
maxConnectionRequestsPerMonthMonthlyConnection requests this monthcreateCompanyConnectionRequest
maxNewsPerMonthMonthlyNews posts this calendar monthcreateNews
maxCvParsesPerMonthMonthlyCV parses this calendar monthEnforced by background-tasks
maxAgentMessagesPerMonthMonthlyAgent chat operations this monthEnforced by background-tasks
maxTextRewritesPerMonthMonthlyText rewrite operations this monthEnforced by background-tasks
info

Total limits count all non-deleted resources. Monthly limits reset on the 1st of each calendar month (UTC). The reset boundary is calculated as Date.UTC(year, month, 1).

warning

The last three AI-related limits (maxCvParsesPerMonth, maxAgentMessagesPerMonth, maxTextRewritesPerMonth) are not enforced by the backend @CheckPlanLimit guard. They are enforced exclusively by the background-tasks BillingGate, which checks plan limits before executing AI operations.

note

maxCompanies is a user-level limit, unlike all other limits which are company-scoped. It counts companies where the user is billingOwner and is checked via PlanLimitsService.checkMaxCompanies(userId) in the createCompany resolver — not through the @CheckPlanLimit guard.

The @CheckPlanLimit Guard

The guard is applied as a decorator on resolver mutations:

@Mutation(() => Job)
@UseGuards(JwtAuthGuard, PlanLimitGuard)
@CheckPlanLimit('maxJobs')
async createJob(@Args('input') input: CreateJobInput, @CurrentUser() user: User) {
// Guard already verified limit not exceeded
}

Guard resolution order:

  1. Extract companyId from mutation variables or user.selectedCompanyId
  2. Look up the company's subscription via billingOwnerId — matches subscriptions with status ACTIVE or TRIALING
  3. Read the relevant limit field from the Plan model
  4. Check for a CompanyBillingConfig override (see below)
  5. Count current usage against the effective limit
  6. Throw PlanLimitExceededException if at or over limit
info

Companies on a free trial (TRIALING status) have the same plan limit enforcement as fully active subscriptions. Trial users get full plan features but are still subject to the limits defined on their plan.

Error Response

When a limit is exceeded, the guard throws a PLAN_LIMIT_EXCEEDED error:

{
"errors": [{
"message": "Plan limit exceeded for maxJobs: 50/50 used on Pro plan",
"extensions": {
"code": "PLAN_LIMIT_EXCEEDED",
"feature": "maxJobs",
"currentUsage": 50,
"limit": 50,
"planName": "Pro"
}
}]
}

No Active Plan

If a company has no billingOwnerId or no active subscription, the guard throws a NO_PLAN_FOUND error. This prevents companies without a subscription from bypassing plan limits entirely.

{
"errors": [{
"message": "No active subscription plan found for company: <companyId>",
"extensions": {
"code": "NO_PLAN_FOUND",
"companyId": "<companyId>"
}
}]
}
tip

This error tells the frontend to prompt the user to subscribe to a plan. It applies to all 13 backend-enforced plan limits.

Plan Usage Summary

The planUsageSummary query returns all 16 limits with current usage for the selected company:

query {
planUsageSummary {
planName
limits {
feature # e.g., "maxJobs"
limit # -1, 0, or specific number
currentUsage # current count
isMonthly # true if resets monthly
isUnlimited # true if limit is -1
}
}
}

Display guidance:

  • When isUnlimited is true, show "Unlimited" instead of a number
  • Calculate percentage: (currentUsage / limit) * 100
  • Show warning styling at 80%+, error styling at 100%

CompanyBillingConfig Overrides

A billing owner managing multiple companies can override plan limits per company via CompanyBillingConfig. This allows uneven distribution of a shared plan's limits.

Effective Limit Resolution

  1. Check CompanyBillingConfig for the company's allocated* field
  2. If the allocated value is set and not -1, use it as the effective limit
  3. Otherwise, fall back to the Plan default

Example: A Pro plan with maxJobs: 10. The billing owner has two companies:

  • Company A: allocatedMaxJobs: 7 -- can create up to 7 jobs
  • Company B: allocatedMaxJobs: 3 -- can create up to 3 jobs

Override Fields

Each of the 16 plan limits has a corresponding allocated* field on CompanyBillingConfig:

Plan FieldOverride Field
maxJobsallocatedMaxJobs
maxTalentPoolsallocatedMaxTalentPools
maxContactsallocatedMaxContacts
maxPipelinesallocatedMaxPipelines
maxInterviewsPerMonthallocatedMaxInterviewsPerMonth
maxMeetingsPerMonthallocatedMaxMeetingsPerMonth
maxCvCollectionsallocatedMaxCvCollections
maxSharedTalentListsallocatedMaxSharedTalentLists
maxCollaborationInvitesallocatedMaxCollaborationInvites
maxConnectionRequestsPerMonthallocatedMaxConnectionRequestsPerMonth
maxUsersPerCompanyallocatedMaxUsersPerCompany
maxNewsPerMonthallocatedMaxNewsPerMonth
maxContactNotesallocatedMaxContactNotes
maxCvParsesPerMonthallocatedMaxCvParsesPerMonth
maxAgentMessagesPerMonthallocatedMaxAgentMessagesPerMonth
maxTextRewritesPerMonthallocatedMaxTextRewritesPerMonth

All override fields default to -1 (use plan default).

Credit Transfer System

Credit pack purchases always land in the user's personal balance. Billing owners distribute credits to company balances via the transferCredits mutation.

Transfer mechanics:

  1. Validates the user is the billing owner of the target company
  2. Locks both balances with SELECT ... FOR UPDATE (pessimistic locking)
  3. Debits the user balance, credits the company balance
  4. Creates two TRANSFER transaction records (one per balance)

The creditAllocationPercent field on CompanyBillingConfig (0--100) configures automatic percentage-based credit distribution per company. The total across all companies for a billing owner cannot exceed 100%.

Monthly Credit Replenishment

A cron job runs daily at 2:00 AM UTC and replenishes credits for active subscriptions:

  1. Finds active company subscriptions (including TRIALING) where Plan.creditsPerMonth > 0
  2. Checks if lastReplenishedAt is more than 30 days ago (or null)
  3. Adds creditsPerMonth credits (additive, not reset) with pessimistic locking
  4. Updates lastReplenishedAt and emits credits.replenished event
info

Credits are additive -- unused credits carry over. The cron adds creditsPerMonth to the existing balance rather than resetting it.

ExceptionHTTPCodeTrigger
PlanLimitExceededException403PLAN_LIMIT_EXCEEDEDFeature usage at plan limit
NoPlanFoundException404NO_PLAN_FOUNDCompany has no active subscription
InsufficientCreditsException402INSUFFICIENT_CREDITSBalance too low for AI operation