Repository navigation
feat: enforce the plan's personal project limit - #624
Merged
Merged
Conversation
The API accepts a couple of personal projects past the plan limit so offline clients can sync at the boundary, so td project create, unarchive and move --to-personal could take a Free account to seven projects. Each now checks the limit first with the SDK's getPersonalProjectCapacity, as the Todoist apps do, and refuses with PROJECT_LIMIT_REACHED. Unarchiving a workspace project and moving an archived one are not checked, since neither adds an active personal project. When the API itself refuses a project (403 MAX_PROJECTS_LIMIT_REACHED), the error now maps to PROJECT_LIMIT_REACHED instead of falling through to AUTH_ERROR with a hint to check the API token.
Brings in getPersonalProjectCapacity, which the project limit check uses.
scottlovegrove
marked this pull request as ready for review
October 31, 2026 13:08
1 of 3 tasks
doistbot
reviewed
Oct 31, 2026
doistbot
left a comment
Member
There was a problem hiding this comment.
This PR adds a client-side personal project limit check before the three writes that can add an active personal project (project create, project unarchive, project move --to-personal), wires the check in correctly after dry-run/preview paths, maps the API's 403 limit tag to PROJECT_LIMIT_REACHED instead of an auth error, and registers getPersonalProjectCapacity as a safe read. Well-scoped and consistent throughout.
Few things worth tightening:
- In
ensurePersonalProjectCapacity,remaining === 0only blocks accounts exactly at the limit; since the API buffer allows accounts to exceed the plan limit,remainingcan be negative — useremaining <= 0so any account without capacity is refused. - Add test coverage for the
!project.isArchivedexemption: moving an archived workspace project with--to-personal --yesshould skip the capacity check even whenremaining: 0, so removing that guard would fail a test.
I also left one optional follow-up note in the details below.
Optional follow-up note (1)
src/commands/project/project.test.ts:2778: Dry-run skip coverage is incomplete. This test covers the no-
--yespreview path, butproject move --to-personal --dry-runis untested, andproject unarchive --dry-run(which returns before the new personal check) has no dry-run test at all. Add dry-run cases with the capacity mock set toremaining: 0, asserting the command resolves andgetPersonalProjectCapacityis not called, so a check placed above the dry-run guards would be caught.
ensurePersonalProjectCapacity now refuses when remaining is zero or less, so it does not rely on the SDK clamping remaining at zero for accounts already over their limit. Adds tests for moving an archived workspace project to personal at the limit (no check), and for the unarchive and move dry runs (no check).
doist-release-bot Bot
added a commit
that referenced
this pull request
Oct 31, 2026
## [5.9.0](v5.8.5...v5.9.0) (2026-10-31) ### Features * enforce the plan's personal project limit ([#624](#624)) ([75dad5e](75dad5e))
Contributor
|
🎉 This PR is included in version 5.9.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
3 tasks done
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.
Summary
The API accepts a couple of personal projects past the plan limit (
USER_LIMIT_BUFFER["PROJECTS"]) so offline clients can sync at the boundary. The Todoist apps check the real limit before writing, buttddidn't, sotd project create,td project unarchiveandtd project move --to-personalcould take a Free account to seven projects. This is the same gap reported for the ChatGPT app in https://github.com/Doist/Issues/issues/21466.ensurePersonalProjectCapacity(src/lib/project-limit.ts) calls the SDK'sgetPersonalProjectCapacityand throwsPROJECT_LIMIT_REACHEDwith an archive/delete/upgrade hint when nothing is left.project create,project unarchive(personal projects only) andproject move --to-personal(active projects only) call it right before the write. Dry runs and the unconfirmed move preview don't.wrapApiErrormaps the API's own refusal (403MAX_PROJECTS_LIMIT_REACHED, reached only if a project is added between the check and the write) toPROJECT_LIMIT_REACHED. It previously fell through toAUTH_ERRORwith a hint to check the API token.getPersonalProjectCapacityis listed as a safe read so read-only tokens can still run the check, and gets a "Checking project limit..." spinner.No commands or flags change, so
SKILL_CONTENTis untouched.Test plan
wrapApiErrormaps the 403 limit tagnpm run type-check,npm run checkandnpm test(2221 passing) on@doist/todoist-sdk15.12.0, which addsgetPersonalProjectCapacity(feat: add getPersonalProjectCapacity for plan project limits todoist-sdk-typescript#736)