Plans & Organizations
Plan Tiers
All plans get access to all 6 stage types. Monetization is volume-based, not feature-gated by stage type.
| Plan | maxActiveJobs | maxCandidatesPerJob | maxInterviewsPerMonth | Premium |
|---|---|---|---|---|
free | 1 | 10 | 30 | — |
starter | 5 | 50 | 200 | — |
pro | 20 | 200 | 1 000 | advancedAnalytics, customBranding |
enterprise | unlimited (-1) | unlimited (-1) | unlimited (-1) | all |
Premium features: advancedAnalytics, customBranding, apiAccess, prioritySupport
Plans are seeded via npm run seed:plans. They are not editable via the UI — changes require a seed re-run or direct DB update.
Organization
Every recruiter account belongs to exactly one Organization. An org is the billing unit, the usage-limit scope, and the resource namespace — all jobs, pipelines, and interviews belong to an org.
On Recruiter Registration (Phase 0)
A single-member org is auto-created:
Organization.create({
name: "<recruiter name>'s Workspace",
planKey: 'free',
featureSnapshot: { maxActiveJobs: 1, maxCandidatesPerJob: 10, ... },
members: [{ userId, role: 'admin' }],
})Multi-Member Org (Phase 5)
Org admins can invite team members via POST /v1/orgs/:id/members/invite. Invited recruiters receive an email, register, and are linked to the org.
Member roles:
| Role | Capabilities |
|---|---|
admin | Full access — billing, member management, all jobs |
recruiter | Create/manage jobs and interviews |
viewer | Read-only access |
featureSnapshot
The Organization.featureSnapshot is a denormalized copy of the org's plan limits. It is re-synced whenever the org's plan changes (via Stripe webhook or manual plan update).
Why a snapshot? Avoids a join to the Plan collection on every API request. The snapshot is always current because it is updated synchronously on every plan change.
// Synced on every plan change:
featureSnapshot: {
maxActiveJobs: plan.features.maxActiveJobs,
maxCandidatesPerJob: plan.features.maxCandidatesPerJob,
maxInterviewsPerMonth: plan.features.maxInterviewsPerMonth,
advancedAnalytics: plan.features.advancedAnalytics,
customBranding: plan.features.customBranding,
apiAccess: plan.features.apiAccess,
}Org Settings API (Phase 5)
GET /v1/orgs/:id — org detail (admin only)
PATCH /v1/orgs/:id — update org name, logo, billingEmail
GET /v1/orgs/:id/members — list members
POST /v1/orgs/:id/members/invite — invite a recruiter by email
PATCH /v1/orgs/:id/members/:userId — change member role
DELETE /v1/orgs/:id/members/:userId — remove member
GET /v1/orgs/:id/features — current featureSnapshot
GET /v1/plans — list all active plans (public, no auth)
POST /v1/orgs/:id/plan — change plan (admin only)Stripe Integration (Phase 5)
POST /v1/webhooks/stripe:
customer.subscription.updated→ sync plan change → updateOrganization.planKey+featureSnapshotcustomer.subscription.deleted→ downgrade tofreeplan
Plan names shown in the billing UI come from GET /v1/plans — never hardcoded in frontend.