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
| Value | Meaning |
|---|---|
-1 | Unlimited (no restriction) |
0 | Feature disabled (blocked entirely) |
>0 | Specific numeric cap |
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
| Feature | Type | What It Counts | Protected Mutation |
|---|---|---|---|
maxCompanies | Total | Companies where user is billing owner | createCompany (user-level check) |
maxJobs | Total | Jobs per company | createJob |
maxTalentPools | Total | Talent pools per company | createTalentPool |
maxContacts | Total | Contacts per company | createContact |
maxPipelines | Total | Pipelines per company | createPipeline |
maxCvCollections | Total | CV collections per company | createCvCollection |
maxSharedTalentLists | Total | Shared talent lists per company | createSharedTalentList |
maxCollaborationInvites | Total | Collaboration invitations per company | createCollaborationInvitation |
maxUsersPerCompany | Total | Team members per company | inviteToCompany |
maxContactNotes | Total | Contact notes per company | createContactNote |
maxInterviewsPerMonth | Monthly | Interviews this calendar month | createInterview |
maxMeetingsPerMonth | Monthly | Meetings this calendar month | createMeeting |
maxConnectionRequestsPerMonth | Monthly | Connection requests this month | createCompanyConnectionRequest |
maxNewsPerMonth | Monthly | News posts this calendar month | createNews |
maxCvParsesPerMonth | Monthly | CV parses this calendar month | Enforced by background-tasks |
maxAgentMessagesPerMonth | Monthly | Agent chat operations this month | Enforced by background-tasks |
maxTextRewritesPerMonth | Monthly | Text rewrite operations this month | Enforced by background-tasks |
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).
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.
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:
- Extract
companyIdfrom mutation variables oruser.selectedCompanyId - Look up the company's subscription via
billingOwnerId— matches subscriptions with statusACTIVEorTRIALING - Read the relevant limit field from the
Planmodel - Check for a
CompanyBillingConfigoverride (see below) - Count current usage against the effective limit
- Throw
PlanLimitExceededExceptionif at or over limit
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>"
}
}]
}
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
isUnlimitedistrue, 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
- Check
CompanyBillingConfigfor the company'sallocated*field - If the allocated value is set and not
-1, use it as the effective limit - Otherwise, fall back to the
Plandefault
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 Field | Override Field |
|---|---|
maxJobs | allocatedMaxJobs |
maxTalentPools | allocatedMaxTalentPools |
maxContacts | allocatedMaxContacts |
maxPipelines | allocatedMaxPipelines |
maxInterviewsPerMonth | allocatedMaxInterviewsPerMonth |
maxMeetingsPerMonth | allocatedMaxMeetingsPerMonth |
maxCvCollections | allocatedMaxCvCollections |
maxSharedTalentLists | allocatedMaxSharedTalentLists |
maxCollaborationInvites | allocatedMaxCollaborationInvites |
maxConnectionRequestsPerMonth | allocatedMaxConnectionRequestsPerMonth |
maxUsersPerCompany | allocatedMaxUsersPerCompany |
maxNewsPerMonth | allocatedMaxNewsPerMonth |
maxContactNotes | allocatedMaxContactNotes |
maxCvParsesPerMonth | allocatedMaxCvParsesPerMonth |
maxAgentMessagesPerMonth | allocatedMaxAgentMessagesPerMonth |
maxTextRewritesPerMonth | allocatedMaxTextRewritesPerMonth |
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:
- Validates the user is the billing owner of the target company
- Locks both balances with
SELECT ... FOR UPDATE(pessimistic locking) - Debits the user balance, credits the company balance
- Creates two
TRANSFERtransaction 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:
- Finds active company subscriptions (including
TRIALING) wherePlan.creditsPerMonth > 0 - Checks if
lastReplenishedAtis more than 30 days ago (or null) - Adds
creditsPerMonthcredits (additive, not reset) with pessimistic locking - Updates
lastReplenishedAtand emitscredits.replenishedevent
Credits are additive -- unused credits carry over. The cron adds creditsPerMonth to the existing balance rather than resetting it.
Related Errors
| Exception | HTTP | Code | Trigger |
|---|---|---|---|
PlanLimitExceededException | 403 | PLAN_LIMIT_EXCEEDED | Feature usage at plan limit |
NoPlanFoundException | 404 | NO_PLAN_FOUND | Company has no active subscription |
InsufficientCreditsException | 402 | INSUFFICIENT_CREDITS | Balance too low for AI operation |