Repository navigation
feat(payments): add per-plan event rate limits and retention - #922
Merged
cameri merged 5 commits intoOct 31, 2026
Merged
Conversation
Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
🦋 Changeset detectedLatest commit: d29dd62 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
Collaborator
Coverage Report for CI Build 41232122419Coverage increased (+0.1%) to 77.356%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats💛 - Coveralls |
[High impact] The PR appears safe to merge; no actionable issues remain.
|
…an for 60s Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
cameri
enabled auto-merge
October 31, 2026 18:25
chappie-daemon
added a commit
that referenced
this pull request
Oct 31, 2026
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## nostream@3.10.0 ### Minor Changes - [#922](#922) [`fcb33de`](fcb33de) Thanks [@Priyanshubhartistm](https://github.com/Priyanshubhartistm)! - feat(payments): add per-plan event rate limits and retention ### Patch Changes - [#1009](#1009) [`ad8c5c4`](ad8c5c4) Thanks [@chappie-daemon](https://github.com/chappie-daemon)! - build: pin the node base image in every file that names one `Dockerfile` pinned `node:24.22.1-alpine` by digest, but `Dockerfile.railwayapp`, `Dockerfile.test`, `docker-compose.yml` and `test/integration/docker-compose.yml` still said `node:24-alpine`. The relay image, the CI image, the Railway image and both migration containers could each start from a different base, and a floating tag moves under a release — so the same commit built twice could run different node code without a change saying so. All five now carry the same tag and digest, and a unit test holds them there: it fails if any reference loses its digest, and it fails if two different references appear. - [#1010](#1010) [`fa54cb0`](fa54cb0) Thanks [@chappie-daemon](https://github.com/chappie-daemon)! - fix(settings): state that rate-limit periods are milliseconds and reject a period that is not one Every shipped limit counts in milliseconds — `1000`, `60000`, `900000`, `3600000` — but nothing said so. `RateLimit.period` is a bare `number`, so an operator writing `period: 60` for a minute got a 60 ms window — and because `rate` is hits per window, that is a limit a thousand times **looser** than intended, honoured without complaint, and caught by nothing. The unit is now stated on `RateLimit` and on every `rateLimits` block in `resources/default-settings.yaml`, and `validateRateLimitPeriods` rejects what cannot be a count of milliseconds: a fraction, a string, a non-number, or anything below one millisecond. A whole number is not enough to tell 60 from 60000, which is why the unit is documented where it is set.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Builds on the subscription lifecycle (#909). Nothing changes unless
payments.enabledis true and a plan setslimits.rateLimitsorlimits.retentionMaxDays(settings and validation already exist from #849).EventMessageHandler.isRateLimiteduses the limits of the sender's plan in place oflimits.event.rateLimitswhile 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.<pubkey>:subscription-plan(plan:<id>ornone) with a TTL that never outlives access, andisUserAdmittedfills 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.deleteExpiredAndRetained) takesplanRetention. Events of subscribers on a plan withretentionMaxDaysare 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.mddescribes 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.maxDaysis 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?
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 byisUserAdmitted, the purge query with plan retention (exact SQL, whitelists, several plans, shorter windows, expired subscribers, invalid values) andMaintenanceService. Reverting the access check, the cache read, the global exclusion, the expired filter or the subscriptions-enabled check each fails a test.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).