Skip to content

feat(invitations): config-gated lifetime cap on invitation creation - #3987

Merged
PierreBrisorgueil merged 3 commits into
masterfrom
feat/3986-invitations-lifetime-cap
Jul 25, 2026
Merged

feat(invitations): config-gated lifetime cap on invitation creation#3987
PierreBrisorgueil merged 3 commits into
masterfrom
feat/3986-invitations-lifetime-cap

Conversation

@PierreBrisorgueil

@PierreBrisorgueil PierreBrisorgueil commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • What changed: Adds a DB-backed lifetime cap per inviter in InvitationsService.create(). It counts ALL invitations ever created by the same invitedBy (any status) and rejects with a 422 AppError (same shape as the existing self-invite guard) once the count reaches config.invitations.maxLifetime.
  • Why: rateLimit.invitationsCreate bounds request rate but not cumulative volume. When billing.referral rewards are enabled, total invitations per account — not rate — is the exposure that matters (credit farming via fake referrer/referee pairs). This closes that gap without touching the burst limiter, which stays out of scope (already wired, per-route profile).
  • Related issues: Closes 🔒 Invitations: config-gated lifetime cap per inviter #3986

Scope

  • Module(s) impacted: invitations (config, repository, service, tests only)
  • Cross-module impact: none
  • Risk level: low

Validation

  • npm run lint
  • npm test (full suite: 213 test suites / 2768 tests passing, incl. new unit + integration coverage)
  • Manual checks done (if applicable) — verified boundary (cap−1 passes) and threshold (cap reached → 422) via both mocked unit tests and a real-DB/real-HTTP integration test

Guardrails check

  • No secrets or credentials introduced (.env*, secrets/**, keys, tokens)
  • No risky rename/move of core stack paths
  • Changes remain merge-friendly for downstream projects — config default is null (absent = disabled), no behavior change until a downstream consumer opts in with a number in their own config layer
  • Tests added or updated when behavior changed — unit (repository + service: capped/boundary/default-off) + integration (real DB count + real HTTP 422 path)

Notes for reviewers

  • Security considerations: the count-then-create check is best-effort, not atomic under concurrent requests at the exact threshold — this mirrors the existing outstanding-pending-invite guard's identical (and already-accepted) race in the same create() function; a hard atomic ceiling would need a separate incrementing counter (schema change), judged out of scope for this fix.
  • Mergeability considerations: none — isolated to the invitations module, no shared/core files touched.
  • Follow-up tasks (optional): none.

Summary by CodeRabbit

  • New Features

    • Added an optional lifetime cap for invitations created by each inviter.
    • When enabled, additional invitations are rejected with a validation error after the configured limit is reached.
    • The cap considers all invitations, regardless of status, and remains disabled by default.
  • Tests

    • Added coverage for enabled, disabled, and edge-case lifetime-cap behavior.

Add a DB-backed lifetime cap per inviter in InvitationsService.create():
counts ALL invitations ever created by the same invitedBy (any status)
and rejects (422, same AppError shape as the self-invite guard) once
the count reaches config.invitations.maxLifetime.

Complements the existing rateLimit.invitationsCreate burst limiter,
which bounds request rate but not cumulative volume — the exposure
that matters when billing.referral rewards are enabled (credit
farming via fake referrer/referee pairs).

Config default: absent/null = disabled (mirrors billing.referral, OFF
at stack level) — no behavior change until a downstream consumer opts
in with a number in their config layer.

Closes #3986
…on coverage

Address PRF Phase 0 review findings on #3986:
- Document the count-then-create race as an accepted best-effort
  characteristic, mirroring the existing outstanding-pending-invite
  guard's identical race in the same function (no atomic counter —
  would need a schema change, out of scope for this fix).
- Add an integration test exercising the real DB count + real HTTP
  422 path (previously covered only by mocked unit tests), plus a
  default-off integration check.
@PierreBrisorgueil PierreBrisorgueil added the Feat A new feature label Jul 25, 2026
@PierreBrisorgueil PierreBrisorgueil self-assigned this Jul 25, 2026
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PierreBrisorgueil, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: be04a5bb-8cea-424a-a149-9eb4f78f2797

📥 Commits

Reviewing files that changed from the base of the PR and between db2dd37 and 7e9185f.

📒 Files selected for processing (1)
  • modules/invitations/config/invitations.development.config.js

Walkthrough

Adds an optional DB-backed lifetime invitation cap per inviter. Invitation creation counts all invitations for the inviter and returns a 422 validation error at the configured threshold. Repository, service, and integration tests cover capped and uncapped behavior.

Changes

Invitation lifetime cap

Layer / File(s) Summary
Invitation counting contract
modules/invitations/repositories/invitations.repository.js, modules/invitations/tests/invitations.repository.unit.tests.js
Adds and exports countByInvitedBy, which counts invitations across all statuses.
Creation-time cap enforcement
modules/invitations/config/invitations.development.config.js, modules/invitations/services/invitations.service.js
Adds nullable maxLifetime configuration and rejects creation with 422 VALIDATION_ERROR when the inviter reaches the configured count.
Cap behavior validation
modules/invitations/tests/invitations.service.unit.tests.js, modules/invitations/tests/invitations.integration.tests.js
Tests disabled, boundary, rejection, unresolved-inviter, and HTTP-level cap behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant InvitationsService
  participant InvitationRepository
  participant InvitationModel
  Client->>InvitationsService: Create invitation
  InvitationsService->>InvitationRepository: Count by inviter
  InvitationRepository->>InvitationModel: Count documents across statuses
  InvitationModel-->>InvitationsService: Return count
  InvitationsService-->>Client: Return 422 at maxLifetime
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: a config-gated lifetime cap on invitation creation.
Description check ✅ Passed The description follows the template well and covers summary, scope, validation, guardrails, and reviewer notes.
Linked Issues check ✅ Passed The changes satisfy #3986 by adding the config-gated lifetime cap, counting all statuses, returning 422, defaulting off, and adding tests.
Out of Scope Changes check ✅ Passed The PR stays within the invitations module and only adds code needed for the lifetime cap and its tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/3986-invitations-lifetime-cap

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@PierreBrisorgueil
PierreBrisorgueil marked this pull request as ready for review July 25, 2026 14:11
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.29%. Comparing base (00768d0) to head (7e9185f).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #3987   +/-   ##
=======================================
  Coverage   93.28%   93.29%           
=======================================
  Files         170      170           
  Lines        5647     5653    +6     
  Branches     1817     1819    +2     
=======================================
+ Hits         5268     5274    +6     
  Misses        306      306           
  Partials       73       73           
Flag Coverage Δ
integration 61.43% <100.00%> (+0.04%) ⬆️
unit 75.53% <100.00%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a8e1cf4...7e9185f. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@PierreBrisorgueil

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 44 minutes.

…bled

Addresses a /critical-review nit on #3986: 0 >= 0 is always true, so
maxLifetime: 0 rejects every invitation immediately — distinct from
null/absent (disabled). Doc-only, no behavior change.
@PierreBrisorgueil
PierreBrisorgueil merged commit 5ac7fbc into master Jul 25, 2026
8 checks passed
@PierreBrisorgueil
PierreBrisorgueil deleted the feat/3986-invitations-lifetime-cap branch July 25, 2026 14:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🔒 Invitations: config-gated lifetime cap per inviter

1 participant