Skip to content

Pause items above the plan's caps on a downgrade and restore them - #1602

Open
helpcodeai wants to merge 3 commits into
feat/plan-fitfrom
feat/plan-caps
Open

helpcodeai wants to merge 3 commits into
feat/plan-fitfrom
feat/plan-caps

Conversation

@helpcodeai

Copy link
Copy Markdown
Contributor

Summary

The Cloud trial now has the Team limits (#1599). A workspace that then buys Starter must end up within Starter's limits without losing anything for good, and without a surprise. This PR adds the automatic downgrade, its undo, and the caps that keep a paid workspace within its plan. It is switched by PLAN_CAPS_ENFORCE, which defaults to dry-run: on the first deploy nothing is refused or paused, and the backend only logs what it would do.

Stacked on #1601 (plan fit), whose ranking it uses, so the paywall warning and the downgrade name the same items.

Changes

  • Plan-change hook in LicenseService. Every Cloud licence write goes through verifyLicense or setLicenseKey: checkout, card-trial conversion, a billing-portal change reported by the site, a key entered by hand. These compare the workspace's current plan before and after and call onPlanChange listeners (fire and forget: a listener never fails or delays the write). Nothing changes on self-hosted. When nothing listens, no extra query runs.
  • PlanCapsService (new plan-caps module):
    • Downgrade, on a plan change to Starter or Team: pauses (isActive=false) the connectors, MCP servers and File Search folders above the caps, least used first (tool calls in the last 30 days); reloads their tools; stores the ids in OrgSettings plan_caps_paused; emails the admins (new transactional template); logs plan_caps_applied.
    • Upgrade: switches back on what is still paused, up to the new caps.
    • Only a plan change triggers it, so paying workspaces already above their cap today are never touched. One reconcile per workspace at a time.
  • Guards (LicenseGuardService), counting only active items:
    • creation of connectors and MCP servers: the trial cap stays hard as before (now with the PLAN_LIMIT code); paid plans are refused only when enforcement is on;
    • new checkCanActivateConnector (File Search folders count against the File Search quota) and checkCanActivateMcpServer, used by PUT /api/connectors/:id and PUT /api/mcp-servers/:id when switching an item back on;
    • new checkCanAddMember, on invitation and on acceptance (existing and new accounts).
    • Refusals are 403 { code: 'PLAN_LIMIT', axis, max, current, message }.
  • GET /api/license/usage returns pausedByPlan: { connectors, mcpServers } with the items still paused, so the dashboard can mark them.
  • AI clients: anythingmcp_list_connectors lists the paused connectors with a hint (choose which stay active at /connectors, or upgrade).
  • Docs: new docs/cloud-plans.md; PLAN_CAPS_ENFORCE in docs/deployment.md; a CLI README note on swapping and PLAN_LIMIT.

Cloud vs self-hosted: Cloud only. On self-hosted, every guard returns early, the listener is not registered and the env var is ignored.

No Prisma schema change: it uses isActive and org_settings.

Type

  • Bug fix
  • New feature
  • Documentation
  • Refactoring
  • Performance improvement
  • Other (describe below)

Testing

  • Added/updated unit tests
  • Tested manually (describe steps below)
  • No tests needed (explain why)

In packages/backend, after npx prisma generate:

  • npx jest --maxWorkers=4 src/license src/plan-caps src/connectors src/mcp-servers src/mcp-server src/adapters/adapters.controller src/adapters/connector-setup src/auth src/settings src/audit: 4,792 passed, 25 skipped, and 13 failed, all in src/auth/jwt-or-management-key.guard.spec.ts. That spec needs a local Postgres and fails the same way on main here (baseline).
  • New tests:
    • plan-caps.service.spec.ts:
      • downgrade pauses the least used, scoped to the org, and sends the email and the event;
      • dry-run only logs;
      • nothing happens when the workspace fits, when enforcement is off, or when the licence ends;
      • an upgrade restores up to the caps;
      • one reconcile per workspace at a time.
    • plan-caps.guard.spec.ts:
      • the trial cap is hard;
      • a paid plan logs in dry-run and refuses when on;
      • unlimited plans and self-hosted are never refused;
      • activation of connectors, servers and File Search folders;
      • members;
      • pausedByPlan.
    • The plan-change announcement in license.service.spec.ts.
    • The email sample: the shared template tests check escaping, links and the text part.
  • npm run lint: 0 errors. The 11 warnings already exist on main.
  • npx tsc --noEmit -p tsconfig.json: clean.
  • node scripts/generate-cli-operations.mjs: no change.

Rollout

  1. Deploy with PLAN_CAPS_ENFORCE unset (dry-run). Watch [plan caps dry-run] log lines and plan_caps_applied events with status=dry-run for a week, and compare them with what the paywall showed.
  2. Email the paying workspaces that are already above their cap (operator step).
  3. Set PLAN_CAPS_ENFORCE=on.

Checklist

  • My code follows the existing code style
  • I have tested my changes locally
  • I have updated documentation (if applicable)
  • All existing tests pass (npm test)
  • This PR has a descriptive title

Related Issues

Third step of the Team-trial plan: #1599 (trial limits), #1601 (plan fit), then this. The dashboard UI is a separate PR on top of #1601.

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