Skip to content

Plans & Organizations


Plan Tiers

All plans get access to all 6 stage types. Monetization is volume-based, not feature-gated by stage type.

PlanmaxActiveJobsmaxCandidatesPerJobmaxInterviewsPerMonthPremium
free11030
starter550200
pro202001 000advancedAnalytics, customBranding
enterpriseunlimited (-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:

RoleCapabilities
adminFull access — billing, member management, all jobs
recruiterCreate/manage jobs and interviews
viewerRead-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.

typescript
// 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 → update Organization.planKey + featureSnapshot
  • customer.subscription.deleted → downgrade to free plan

Plan names shown in the billing UI come from GET /v1/plans — never hardcoded in frontend.