Release Notes — April 3, 2026
This release completes the downgrade over-limit enforcement system — the full set of backend, frontend, email, and UI changes that ensure the app correctly handles situations where a unit's existing data exceeds the limits of its new (lower) plan after a subscription downgrade.
Downgrade Enforcement: Complete Over-Limit Resource Handling
Overview
When a workspace owner downgrades from PRO to FREE (or ENTERPRISE to PRO), their existing data may exceed the new plan's limits. This release ensures every layer of the system — API, UI, notifications — correctly enforces and communicates those limits.
Affected limits:
| Resource | FREE limit | PRO limit |
|---|---|---|
| Team Members | 3 | 20 |
| Projects (per workspace) | 3 | Unlimited |
| Tasks (per workspace) | 15 | 70 |
| Time Entries (per workspace) | 100 | 1,000 |
Backend Enforcement
Write Block for Locked Users
All write endpoints (POST, PUT, PATCH, DELETE) on projects, tasks, team, and time entries are now protected by the requireNotOverLimitLocked middleware. Users whose account has been locked due to a downgrade receive a 403 ACCOUNT_LOCKED response on any write attempt.
Timer Start Block
The startTimer() endpoint enforces the time entry limit for the workspace. Attempting to start a new timer when the workspace is at or over its time entry limit returns 403 LIMIT_EXCEEDED.
Team Invite Block
The inviteUser() endpoint checks maxTeamMembers against the current tier before sending an invitation. Invitations over the limit return 403 LIMIT_EXCEEDED.
Member Lock on Downgrade (Webhook)
When a customer.subscription.updated or customer.subscription.deleted event is received and the tier decreases, markExcessMembersAsLocked() runs automatically:
- Queries all
activeandpendingmembers (both count toward usage) - Locks the newest members exceeding the new limit
- Sends notification emails to each locked member
Email Notifications
Downgrade Summary Email (Owner)
The workspace owner receives an email immediately on downgrade listing all over-limit resources:
- Number of locked team members
- Number of excess projects
- Links to resolve each issue (Team, Projects, Upgrade)
Access Limited Email (Locked Members)
Each member whose access is locked receives a personal notification email explaining:
- The account was downgraded
- Their access is now limited
- Who to contact to restore full access
Frontend: Over-Limit UI
Global Locked User Banner
Users with isOverLimitLocked: true see a persistent warning banner across the entire application (rendered in AppShell):
"Your account access is limited due to a plan downgrade. Contact the workspace owner to restore access."
Over-Limit Banners on All Resource Pages
| Page | Banner condition |
|---|---|
| Projects | current > max projects |
| Tasks | current >= max tasks |
| Time Tracking | current >= max time entries |
| Team Members | current > max members (fixed: was >=) |
All banners include an Upgrade action button.
Excess Project Read-Only Banners
- Project Details page — a warning banner appears when the project is marked
isExcessProject: true, with an explanation and a link to resolve - Project Tasks tab — task creation is disabled with a banner explaining the project is in read-only mode
Selector Disabled States
Locked/excess resources are visually disabled (not hidden) in all selector dropdowns:
| Component | Disabled items |
|---|---|
| Create Task — Project | isExcessProject: true → 🔒 "Over limit" badge |
| Edit Task — Project | isExcessProject: true → 🔒 "Over limit" badge |
| Create Task — Assignees | isOverLimitLocked: true → 🔒 "Access limited" badge |
| Edit Task — Assignees | isOverLimitLocked: true → 🔒 "Access limited" badge |
| Timer / Manual Entry — Project/Task | isExcessProject: true → 🔒 "Over limit" badge |
Over-Limit Banner — Strict > Fix
Problem: The "Over limit" warning banner on the Team Members page appeared at exactly 3/3 (at the limit), not only when strictly over.
Fix: The banner now uses current > max (strictly over). Being at the limit is normal; being over is an exceptional state requiring action.
Bug Fixes
Member Lock Query: Pending Members Included
markExcessMembersAsLocked() previously only queried status: 'active' members. Since pending members also count toward the usage limit in UsageTrackingService, this caused the lock logic and the counter to disagree.
Fix: The query now uses { status: { $in: ['active', 'pending'] } }.
Unit Model Tier Limits Corrected
The Unit model's pre-save hook had hardcoded values (FREE: 5 members, PRO: 25 members) that conflicted with the authoritative tiers.config.ts values.
Fix: Pre-save hook now uses the correct values: FREE: 3, PRO: 20.
Developer / Ops Tools
manual-downgrade-to-free.mjs Script
Manually downgrades a unit to FREE in the database — useful for testing and support workflows without triggering a real Stripe event.
node scripts/manual-downgrade-to-free.mjs <unitBundle>
Actions: Sets tier, limits, cancelAtPeriodEnd, currentPeriodEnd, and runs markExcessMembersAsLocked() immediately.
test-downgrade-enforcement.mjs Script
Simulates the full downgrade/upgrade enforcement cycle without Stripe.
node scripts/test-downgrade-enforcement.mjs <unitBundle> [status|downgrade|upgrade]
| Scenario | Effect |
|---|---|
status | Show current lock state of all members |
downgrade | Simulate PRO → FREE, lock excess members |
upgrade | Simulate upgrade back, unlock all members |
Summary
| Area | Change |
|---|---|
| Backend | startTimer() limit check — timer already enforced (confirmed) |
| Backend | inviteUser() team member limit check — already enforced (confirmed) |
| Backend | requireNotOverLimitLocked middleware — applied to all write routes (projects, tasks, team, time-entries) |
| Backend | markExcessMembersAsLocked now includes pending members |
| Backend | Unit.model.ts pre-save hook limits corrected (FREE: 3, PRO: 20) |
| Backend | Downgrade webhook sends summary email to owner |
| Backend | Locked members receive access-limited notification email |
| Frontend | Team Members page banner: >= → > (strict over-limit) |
| Frontend | Create Task dialog: locked members shown as disabled with badge |
| Frontend | Edit Task dialog: locked members shown as disabled with badge |
| Frontend | Create Task dialog: excess projects shown as disabled with badge |
| Frontend | Edit Task dialog: excess projects shown as disabled with badge |
| Frontend | Project/Task selector: locked resources shown as disabled with badge |
| Frontend | AppShell.tsx: global persistent banner for locked users |
| Frontend | ProjectDetailsPage: read-only banner on excess projects |
| Frontend | ProjectTasksTab: task creation disabled banner on excess projects |
| Frontend | TimeTrackingPage: over-limit banner + timer widget block |
| Frontend | ProjectsPage: over-limit banner |
| Frontend | TaskManagementPage: over-limit banner |
| Scripts | manual-downgrade-to-free.mjs added |
| Scripts | test-downgrade-enforcement.mjs added |
Availability: All plans (enforcement automatically applies on downgrade)