Skip to content

feat(payments): add per-plan event rate limits and retention - #922

Open
Priyanshubhartistm wants to merge 2 commits into
cameri:mainfrom
Priyanshubhartistm:feat/subscription-tier-limits
Open

Priyanshubhartistm wants to merge 2 commits into
cameri:mainfrom
Priyanshubhartistm:feat/subscription-tier-limits

Conversation

@Priyanshubhartistm

Copy link
Copy Markdown
Collaborator

Description

Builds on the subscription lifecycle (#909). Nothing changes unless payments.enabled is true and a plan sets limits.rateLimits or limits.retentionMaxDays (settings and validation already exist from #849).

  • Rate limits: EventMessageHandler.isRateLimited uses the limits of the sender's plan in place of limits.event.rateLimits while the subscription gives access (grace included). An empty list means no limit. Everyone else, and a subscriber whose subscription ended or whose plan was removed from the settings, keeps the global limits. Counters are keyed exactly as before (<pubkey>:events:<period>), whitelists still apply.
  • The plan comes from the admission cache: it is cached under <pubkey>:subscription-plan (plan:<id> or none) with a TTL that never outlives access, and isUserAdmitted fills it when it reads the subscription, so there is no extra database read per event. If a read fails the global limits apply and nothing is cached.
  • Retention: the purge (deleteExpiredAndRetained) takes planRetention. Events of subscribers on a plan with retentionMaxDays are purged at the plan's window, everyone else at the global one, with the kind and pubkey whitelists unchanged. A subscriber counts only while their access lasts, so once it ends their events fall back to the global window at the next pass. The query is the same as before when no plan sets a retention.
  • CONFIGURATION.md describes both settings.

Related Issue

Closes #921

Motivation and Context

Rate limits and retention were global, so a premium subscriber got the same limits as everyone else. This is the "higher tiers get better rate limits and longer event retention" part of the tiered access.

A plan retention only applies while limits.event.retention.maxDays is set (the purge does not run without it). Per-plan connection and subscription-count limits are not part of this PR.

How Has This Been Tested?

  • New unit tests: the plan limits in isRateLimited (plan vs global, empty list, kinds, expired, lapsed and grace, removed plan, whitelist, subscriptions off, cache hit/miss/error/TTL, database error), the plan cached by isUserAdmitted, the purge query with plan retention (exact SQL, whitelists, several plans, shorter windows, expired subscribers, invalid values) and MaintenanceService. Reverting the access check, the cache read, the global exclusion, the expired filter or the subscriptions-enabled check each fails a test.
  • Ran the purge on a real PostgreSQL 16 with the migrations applied (global 7 days, premium 90 days): an active premium subscriber and one in grace keep their 3 and 20 day old events and lose the 100 and 400 day old ones; an expired premium subscriber and a user without a plan keep only the 3 day old ones.
  • pnpm lint, pnpm check:deps, pnpm run build:check, pnpm run build, pnpm run verify:cli:build, pnpm run test:unit (2547 passing), pnpm run test:cli (105 passing).

Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
@changeset-bot

changeset-bot Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a279295

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
nostream Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coveralls

coveralls commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 39581695383

Coverage increased (+0.2%) to 74.378%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 66 of 66 lines across 3 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 11313
Covered Lines: 8811
Line Coverage: 77.88%
Relevant Branches: 5942
Covered Branches: 4023
Branch Coverage: 67.7%
Branches in Coverage %: Yes
Coverage Strength: 795.84 hits per line

💛 - Coveralls

@greptile-apps

greptile-apps Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issues were found.

Summary

Adds per-plan event rate limits and retention while keeping global settings as the fallback.

  • Active subscribers use their plan’s event rate limits.
  • Scheduled purges use each active plan’s event retention window.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Event[Incoming event] --> Limits[Read cached plan or subscription]
  Limits --> Rate[Apply plan limits or global limits]
  Rate --> Admission[Check admission using the shared read]
  Admission --> Save[Save accepted event]
  Purge[Retention purge] --> Access{Subscriber has access and plan retention?}
  Access -->|Yes| Plan[Apply plan retention]
  Access -->|No| Global[Apply global retention]
Loading

Reviews (2) · Last reviewed commit: "fix(payments): read the subscription onc..." · Reviewed by Greptile

Comment thread src/handlers/event-message-handler.ts Outdated
Comment thread src/handlers/event-message-handler.ts Outdated
…an for 60s

Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(payments): per-plan event rate limits and retention

2 participants