FM-045 — Cross-Workspace Personal Surfaces Hit requireAppEnabled 403s
App gates applied per-workspace, but rollup surfaces (Command Center, Home) legitimately span workspaces. Any disabled app caused 403 cascade.
| Field | Value |
|---|---|
| Severity | SEV-2 (cross-workspace rollups silently broken) |
| Category | API Authorization / Cross-workspace |
| Date | 2026-05-04 |
| Status | Archived |
Symptom
Section titled “Symptom”Cross-workspace rollups (Command Center, Home focus list, “Also needs attention”, inline task panel) silently fail with 403s for tasks in workspaces where the relevant app is disabled. Presents as “Could not load this task”, “Failed to complete a task”, or AppDisabledPage on navigate.
Root cause
Section titled “Root cause”Per-workspace app gates (requireAppEnabled('tasks')) are applied at the route mount, but rollup surfaces are cross-workspace personal-productivity views. The moment a user has an app off in any workspace, every endpoint backing the rollup 403s for those rows.
The class is bigger than tasks — any future cross-workspace rollup that hits per-workspace-app-gated endpoints will hit the same wall.
Fix Pattern
Section titled “Fix Pattern”Server: factor personal-edit endpoints (GET by id, PATCH by id, archive, lifecycle mutations) out of the gated default router into a sibling ungated “personal lifecycle” router. Mount on same path before gated mount. Multi-tenancy stays enforced by organizationContext membership and id+organizationId scoping.
Client: rollup row’s “Open in app” affordance must check the target workspace’s app-enabled state, not current. Use useAppAccessForOrg(targetWorkspaceId) and hide the link when disabled.
Prevention
Section titled “Prevention”- Before adding
requireAppEnabledto single-resource GET/PATCH/DELETE: ask “Does any cross-workspace surface call this for items from other workspaces?” If yes, factor onto ungated lifecycle router. - Before adding “Open in app” navigation from a cross-workspace rollup: gate on
useAppAccessForOrg(targetWorkspaceId).access[app]. Default to hidden, not to broken navigation. - Search proactively: grep
server/routes.tsforrequireAppEnabledcalls on tasks / goals / projects / calendar — each line is a candidate.