← all branches

feat/mod-gates

needs attention
f9f2669 · incrementalPR #294reviewed 2026-07-10 16:48 UTC0H · 2M · 4L · 5I
The branch
Purpose
Fix prod bug where enabling Tarifas per-org had no visible effect for non-admin members; extend per-org module entitlements to Sites and Assets.
Goal
Make soft-launch gate entitlement-aware so platform admins can activate/deactivate all three gated modules per organization with immediate effect.
Sub-goals
  • SG-1: ORG_MODULES + OrgModuleKeySchema widened (tarifas, sites, assets)
  • SG-2: soft-launch.ts entitlement-aware; guard + layout + sidebar + admin toggles + tests
  • SG-3: Preview test bed (in progress)
The changes (whole branch)
What
Review-fix pass: OrgModuleKey type import replacing local union; tri-state enabledModules signal consolidated in dashboard layout; credentials tab now receives enabledModules for tab visibility; sidebar requiredModule logic fixed to === 'live' (was negated 'greyed'/'hidden'); GATED_PATH_MODULES gets explicit OrgModuleKey type annotation; doc updates.
Why
Second commit tightens type safety, fixes a sidebar requiredModule bug (hidden items were doubly-gated), consolidates the enabledModules derivation to a single signal in layout.tsx, and ensures credentials tab visibility matches SoftLaunchGuard behavior for entitled orgs.
Areas
.claude/rules/api-patterns.md+130apps/platform/CLAUDE.md+11apps/platform/src/app/[locale]/(dashboard)/admin+128apps/platform/src/app/[locale]/(dashboard)/credentials+126apps/platform/src/app/[locale]/(dashboard)+1812apps/platform/src/components/DashboardSidebar.tsx+1816apps/platform/src/lib/soft-launch.ts+263
Blast
7 files, +100/−46 in this commit; cumulative branch 13 files, +256/−63. All changes contained to platform app UI layer and documentation — no schema migrations, no domain logic, no API contract changes.
assets WRITE endpoints remain unenforced server-side — explicitly documented as pre-global-reveal debt SG-3 preview verification not yet complete (non-admin user test)
typecheck· pnpm typecheck passes per PR descriptionunit-tests· soft-launch.test.ts — 14 tests pass (from main feature commit)ci· CI status not accessible via tokencoderabbit· no .coderabbit.yaml present

Findings · 12

security4

info

requiredModule fix is security-neutral — client-side UX, by design

apps/platform/src/components/DashboardSidebar.tsx:94

Old/new code produce the same visible-nav outcome for every nav state. Hidden items are excluded either via the hidden flag (new) or the requiredModule entitlement filter (old). Net security delta: zero. The system's declared model is client-side UX; SoftLaunchGuard at the layout level is the route gate.

info

useOrganization(user?.orgId) with undefined orgId safely disables the query

apps/platform/src/app/[locale]/(dashboard)/credentials/layout.tsx:25

The hook has `enabled: !!publicId` — when user?.orgId is undefined, the query is disabled and no request is made. No risk of fetching with an undefined org ID or hitting another org.

info

enabledModules sourced from server-enforced org membership — no cross-org spoofing possible

apps/platform/src/app/[locale]/(dashboard)/layout.tsx:76

GET /api/organizations/:publicId calls requireOrgAccess(..., 'member') before returning data. A user cannot receive another org's enabledModules by supplying a foreign publicId — they would fail the membership check and receive 403/404.

info

Fail-closed tri-state on org fetch error correctly degrades to deny

apps/platform/src/app/[locale]/(dashboard)/layout.tsx:76

orgError → enabledModules = [] → gated routes redirect to /bills/contratos. Correct security posture for a fail-closed gate.

conventions2

info

`import type` comment explains WHAT, not WHY

apps/platform/src/app/[locale]/(dashboard)/admin/_components/OrgDetailPanel.tsx:44

The first sentence ('Type-only import — erased at compile time, safe for the client bundle') restates what `import type` communicates by name. The genuinely non-obvious part is the second sentence (forbidden to import ORG_MODULES *value*). Trim to the non-obvious WHY only.

info

GATED_PATH_MODULES loses literal-string narrowing after removing `as const`

apps/platform/src/lib/soft-launch.ts:44

With `as const` a path-string typo (e.g. '/sties') would fail to compile. With the explicit `readonly (readonly [string, OrgModuleKey])[]` annotation, path strings widen to `string` — typos are silently accepted. Safe for 5 entries; worth restoring `as const` if the table grows.

tests3

medium

requiredModule fix in DashboardSidebar untested at component level

apps/platform/src/components/DashboardSidebar.tsx:94

The fix (`=== 'live' ? 'sites' : undefined` replacing `=== 'greyed' ? undefined : 'sites'`) is correct, but the downstream `visibleNavItems` filter that consumes `requiredModule` has no component test. The old code had a concrete bug for `hidden` modules (item was doubly-gated by entitlement AND hidden flag); the new code is correct. Without a test, any future refactor of the ternary direction reintroduces the bug silently. A narrow sidebar unit test covering all three nav-state values per module would pin this.

medium

credentials/layout showMetricsTab wiring fix has no callsite test

apps/platform/src/app/[locale]/(dashboard)/credentials/layout.tsx:36

The pure `isGated('/credentials/metrics', false, ['assets'])` contract is covered by soft-launch.test.ts. What's missing is a component/render test that verifies `CredentialsLayout` actually passes `org?.enabledModules` to `isGated`. The bug being fixed had exactly this shape — missing argument at a specific callsite. A narrow RTL render (mock useOrganization returning enabledModules: ['assets'], assert Metrics tab present) would close the regression window.

low

Tri-state enabledModules derivation in layout.tsx has no unit test

apps/platform/src/app/[locale]/(dashboard)/layout.tsx:76

The three-branch expression is minimal and fail-closed. Pure underlying functions (isGated, navState) are thoroughly tested. Acceptable to leave without a test — noted for completeness.

improvement3

medium

credentials/layout re-fetches org data via implicit cache-key contract

apps/platform/src/app/[locale]/(dashboard)/credentials/layout.tsx:27

The sub-layout calls `useOrganization(user?.orgId)` independently and relies on an inline comment for correctness ('same query key + staleTime as the dashboard layout — served from cache'). This is a fragile implicit contract: if the dashboard layout's query key or staleTime ever changes, this silently refetches or returns stale data. The right fix is to thread `enabledModules` through context (extend `OrganizationContext` or add a thin `ModuleEntitlementContext`). Not a current runtime bug, but a maintainability risk.

low

Double null-coalescing: `enabledModules ?? []` passed to sidebar that already does `?? []` internally

apps/platform/src/app/[locale]/(dashboard)/layout.tsx:161

DashboardSidebar already does `const modules = enabledModules ?? []` at line 78. The external `?? []` collapses the tri-state before it enters the sidebar, masking the undefined-in-flight signal. Passing `enabledModules` directly (without `?? []`) would be cleaner. Not incorrect — sidebar currently renders greyed correctly either way.

low

Rename `orgModules` → `modules` loses specificity

apps/platform/src/components/DashboardSidebar.tsx:78

`orgModules` communicated the value is the org's enabled modules vs platform flags. `modules` is generic. In a component that mixes platform-admin bypass with per-org entitlement logic, the original name was more self-documenting. Cosmetic, no behavior change.

History · 2 commits

  1. f9f2669needs attentionincremental0H · 2M · 4L2026-07-10 16:48current
  2. 3810b89needs attentionfull7H · 10M · 10L2026-07-10 16:32