Skip to content

Enforce member cap on imports + warn before exceeding it#77

Merged
SiteRelEnby merged 1 commit into
mainfrom
import-member-cap
May 25, 2026
Merged

Enforce member cap on imports + warn before exceeding it#77
SiteRelEnby merged 1 commit into
mainfrom
import-member-cap

Conversation

@SiteRelEnby
Copy link
Copy Markdown
Contributor

Summary

Imports bypassed the per-account member cap entirely. Every importer added members directly via db.add, so a large import sailed past the limit (free tier = 512) and the account only hit the wall on the next manual "Add member" (403). This enforces the cap on every import path (hard-fail up front) and warns in the UI before it would happen.

Per the agreed behaviour: over-cap imports hard-fail the whole job rather than partially filling.

Backend

  • New member_limits service as the single source of truth: get_member_limit (tier default, per-user member_limit override wins, 0 = unlimited), count_members, and enforce_import_member_cap which raises ImportPayloadError (a classified, clean job failure) when an import would overflow. The normal POST /v1/members create path now uses the same helper, so enforcement can't drift between places.
  • All four importers (Sheaf, PluralKit, SimplyPlural, Tupperbox) call the check after applying the member selector, before writing anything. SimplyPlural counts custom fronts too, since they become Member rows and count toward the cap (matching the create path).
  • New GET /v1/members/limit returning {limit, current, remaining} (remaining null when unlimited).

Frontend

  • Each import flow fetches the limit, shows a warning, and disables the Import button when the current selection would exceed the remaining headroom. Deselecting members (or using the existing member selector) clears it. The backend hard-fail is the backstop if the warning is ignored or the API is hit directly.

Tests

  • An import over an overridden cap hard-fails and writes nothing.
  • The limit endpoint shape holds across tier configs (self-hosted unlimited / SaaS numeric cap).

Migrations

None. Reuses the existing user.member_limit column, the member_limit_* config, and a member COUNT(*).

Imports bypassed the per-account member cap entirely: every importer
added members directly, so a large import sailed past the limit and the
account only hit the wall on the next manual add. Now every import path
hard-fails up front if it would exceed the cap, and the UI warns first.

- New member_limits service: single source of truth for the effective
  limit (tier default, per-user override wins, 0 = unlimited), the
  current count, and enforce_import_member_cap which raises
  ImportPayloadError (a clean job failure) when an import would overflow.
  The normal create path now uses the same helper.
- All four importers (Sheaf, PluralKit, SimplyPlural, Tupperbox) call the
  check after applying the member selector, before writing anything. SP
  counts custom fronts too, since they are members and count toward the
  cap.
- New GET /v1/members/limit (limit / current / remaining) so the import
  flows can warn. Each flow shows a warning and disables Import when the
  selection would exceed the remaining headroom; deselecting members
  clears it.

Tests: import over an overridden cap hard-fails and writes nothing; the
limit endpoint shape holds across tier configs.
@SiteRelEnby SiteRelEnby merged commit 351124e into main May 25, 2026
2 checks passed
@SiteRelEnby SiteRelEnby deleted the import-member-cap branch May 25, 2026 18:54
@SiteRelEnby SiteRelEnby mentioned this pull request May 26, 2026
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.

1 participant