Skip to main content

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:

ResourceFREE limitPRO limit
Team Members320
Projects (per workspace)3Unlimited
Tasks (per workspace)1570
Time Entries (per workspace)1001,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 active and pending members (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

PageBanner condition
Projectscurrent > max projects
Taskscurrent >= max tasks
Time Trackingcurrent >= max time entries
Team Memberscurrent > 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:

ComponentDisabled items
Create Task — ProjectisExcessProject: true → 🔒 "Over limit" badge
Edit Task — ProjectisExcessProject: true → 🔒 "Over limit" badge
Create Task — AssigneesisOverLimitLocked: true → 🔒 "Access limited" badge
Edit Task — AssigneesisOverLimitLocked: true → 🔒 "Access limited" badge
Timer / Manual Entry — Project/TaskisExcessProject: 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]
ScenarioEffect
statusShow current lock state of all members
downgradeSimulate PRO → FREE, lock excess members
upgradeSimulate upgrade back, unlock all members

Summary

AreaChange
BackendstartTimer() limit check — timer already enforced (confirmed)
BackendinviteUser() team member limit check — already enforced (confirmed)
BackendrequireNotOverLimitLocked middleware — applied to all write routes (projects, tasks, team, time-entries)
BackendmarkExcessMembersAsLocked now includes pending members
BackendUnit.model.ts pre-save hook limits corrected (FREE: 3, PRO: 20)
BackendDowngrade webhook sends summary email to owner
BackendLocked members receive access-limited notification email
FrontendTeam Members page banner: >=> (strict over-limit)
FrontendCreate Task dialog: locked members shown as disabled with badge
FrontendEdit Task dialog: locked members shown as disabled with badge
FrontendCreate Task dialog: excess projects shown as disabled with badge
FrontendEdit Task dialog: excess projects shown as disabled with badge
FrontendProject/Task selector: locked resources shown as disabled with badge
FrontendAppShell.tsx: global persistent banner for locked users
FrontendProjectDetailsPage: read-only banner on excess projects
FrontendProjectTasksTab: task creation disabled banner on excess projects
FrontendTimeTrackingPage: over-limit banner + timer widget block
FrontendProjectsPage: over-limit banner
FrontendTaskManagementPage: over-limit banner
Scriptsmanual-downgrade-to-free.mjs added
Scriptstest-downgrade-enforcement.mjs added

Availability: All plans (enforcement automatically applies on downgrade)