Skip to content

fix(dashboard): poll Clerk until externalOrgId sync on CLI auth page#11434

Merged
scopsy merged 5 commits into
nextfrom
fix/dashboard-cli-auth-org-sync
Jun 3, 2026
Merged

fix(dashboard): poll Clerk until externalOrgId sync on CLI auth page#11434
scopsy merged 5 commits into
nextfrom
fix/dashboard-cli-auth-org-sync

Conversation

@scopsy
Copy link
Copy Markdown
Contributor

@scopsy scopsy commented Jun 3, 2026

Summary

  • Fixes the CLI authorize page (/cli/auth) staying on a loading spinner after a new user signs up, creates an org, and returns from onboarding without a manual refresh.
  • Adds useSyncClerkExternalOrgId to poll organization.reload() until Novu's externalOrgId appears in Clerk publicMetadata, then environment and API key queries can run.
  • Shows a clear message if org sync exceeds ~60s instead of spinning indefinitely.

Why

After org creation, the backend writes externalOrgId into Clerk asynchronously. Until then, currentOrganization._id is empty, useFetchEnvironments stays disabled (staleTime: Infinity), and isLoading never clears. A full page refresh worked because Clerk reloaded with synced metadata.

sequenceDiagram
  participant User
  participant CliAuth as /cli/auth
  participant Clerk
  participant API as Novu API

  User->>CliAuth: Return after signup + org create
  CliAuth->>Clerk: useOrganization (no externalOrgId yet)
  loop Poll until synced
    CliAuth->>Clerk: organization.reload()
    Clerk-->>CliAuth: publicMetadata.externalOrgId
  end
  CliAuth->>API: GET environments
  API-->>CliAuth: Development env + API key
  User->>CliAuth: Authorize
Loading

Test plan

  • Run npx novu@latest connect as a new user: sign up, create org, land on /cli/auth, Authorize button leaves loading without refresh.
  • Existing user with org: CLI auth still loads and authorizes normally.
  • Invalid/missing device_code still shows the existing error state.

Linear

Todo: Link Linear issue — please create a ticket and update the PR title to include fixes NOV-XXXX.

What changed

The CLI authorize page no longer gets stuck on a loading spinner after new users sign up, create an organization, and return from onboarding. A new bootstrap hook polls Novu's environment data and Clerk's metadata until the external organization ID syncs, preventing the UI from spinning indefinitely. This centralized polling logic replaces ad-hoc implementations across multiple pages.

Affected areas

dashboard: Added a new useBootstrapOrganization() hook that coordinates organization bootstrap by polling Novu environments with a 1-second interval until Clerk's external org ID is available, then reloads Clerk user and organization data. Integrated the hook into EnvironmentProvider to gate environment and API-key queries on organization readiness. Updated cli-auth.tsx to derive loading state from environment initialization and added explicit timeout guards. Refactored agents-setup-page.tsx to use the centralized bootstrap flow instead of component-local polling, simplifying data-readiness checks.

Key technical decisions

  • Polling Novu environments in EnvironmentProvider rather than repeatedly calling Clerk.reload(), following the inbox onboarding pattern.
  • Centralizing organization sync logic in a reusable hook to avoid ad-hoc implementations across pages.
  • Using a bootstrap flag to detect when Clerk organization is not yet synced, enabling conditional environment polling until readiness.

Testing

Manual verification is required: verify new-user CLI connect succeeds without manual page refresh, confirm existing users are unaffected, and validate that existing device-code errors are handled correctly. No new automated tests are mentioned in the PR.

Greptile Summary

This PR fixes the CLI authorize page getting stuck on an indefinite loading spinner after a new user signs up, creates an org, and returns from onboarding. The root cause was that Novu's backend writes externalOrgId into Clerk asynchronously, leaving currentOrganization._id empty and the environments query disabled until a full page refresh.

  • Introduces useBootstrapOrganization in the environment context: polls getEnvironments every second (with a dummy cache key 'org') and, once Novu confirms the org is ready (environments returned), calls clerkUser.reload() + clerkOrganization.reload() once to sync externalOrgId into Clerk — replacing the previous, more fragile Clerk-side polling approach.
  • Removes the duplicated bootstrap/polling logic from AgentsSetupPage (which was doing the same thing inline) and wires cli-auth.tsx through areEnvironmentsInitialLoading from the shared environment context.

Confidence Score: 5/5

The change is safe to merge; it correctly resolves the indefinite-spinner bug for new users without regressing existing-user flows.

The bootstrap hook guard conditions (needsBootstrap, hasReloadedRef) are correct and BOOTSTRAP_ORG_CACHE_KEY is a React Query key only — getEnvironments authenticates via session and ignores it. The isAuthorizeDataLoading computation in cli-auth handles all transitions correctly: during bootstrap the main environments query is disabled so areEnvironmentsInitialLoading is false, but !currentEnvironment keeps the flag true; after Clerk reloads the flag transitions cleanly. The only open concern is the missing polling timeout, a non-regression edge case that does not block correctness on the happy path.

No files require special attention for correctness; use-bootstrap-organization.ts is the only file worth a follow-up for the missing timeout guard.

Important Files Changed

Filename Overview
apps/dashboard/src/context/environment/use-bootstrap-organization.ts New hook that polls Novu's environments endpoint until org is provisioned, then reloads Clerk. Logic is correct but lacks a timeout if environments never arrive.
apps/dashboard/src/context/environment/environment-provider.tsx Wires useBootstrapOrganization into the provider; exposes areEnvironmentsInitialLoading through context correctly.
apps/dashboard/src/pages/cli-auth.tsx Replaces the old isLoading flag with isAuthorizeDataLoading that correctly tracks areEnvironmentsInitialLoading, API key loading, and missing currentEnvironment.
apps/dashboard/src/pages/agents-setup-page.tsx Removes the inline useAgentEnvLoading hook and manual Clerk reload; delegates bootstrap to EnvironmentProvider. isDataReady now correctly requires both currentOrganization._id and currentEnvironment.

Sequence Diagram

sequenceDiagram
  participant User
  participant CliAuth as /cli/auth
  participant EnvProvider as EnvironmentProvider
  participant Bootstrap as useBootstrapOrganization
  participant NovuAPI as Novu API
  participant Clerk

  User->>CliAuth: Return after signup + org create
  CliAuth->>EnvProvider: "mount (currentOrg._id = empty)"
  EnvProvider->>Bootstrap: "needsBootstrap = true"
  loop Poll every 1000ms
    Bootstrap->>NovuAPI: GET /environments (session auth)
    NovuAPI-->>Bootstrap: [] empty (org not ready yet)
  end
  NovuAPI-->>Bootstrap: [dev, prod] (org provisioned)
  Bootstrap->>Clerk: clerkUser.reload() + clerkOrganization.reload()
  Clerk-->>EnvProvider: "currentOrg._id = real-id"
  EnvProvider->>NovuAPI: GET /environments (real org)
  NovuAPI-->>EnvProvider: [dev, prod]
  EnvProvider->>CliAuth: currentEnvironment set, areEnvironmentsInitialLoading false
  CliAuth->>NovuAPI: GET /environments/apiKeys
  NovuAPI-->>CliAuth: apiKey
  User->>CliAuth: Authorize
Loading

Fix All in Cursor

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/dashboard/src/context/environment/use-bootstrap-organization.ts:26-30
**No upper bound on bootstrap polling**

`refetchInterval: 1000` runs forever as long as `needsBootstrap` is true and `environments` returns an empty array. If Novu's org provisioning succeeds but environment creation fails (or the endpoint returns `[]` persistently), the user is left with an infinite spinner. The PR description called out a ~60 s timeout in the earlier Clerk-polling design; that safeguard wasn't carried over here. Adding a `MAX_POLL_MS` ref (e.g. 60 000 ms) and disabling `refetchInterval` after it elapses — then surfacing a "refresh the page" message via a state flag — would restore the escape hatch for this edge case.

Reviews (4): Last reviewed commit: "fix(dashboard): bootstrap org via Novu e..." | Re-trigger Greptile

After signup and org creation, the CLI authorize page stayed on a loading
spinner until refresh because environment queries wait on externalOrgId in
Clerk metadata. Poll organization.reload() until the org is synced.
@netlify
Copy link
Copy Markdown

netlify Bot commented Jun 3, 2026

Deploy preview added

Name Link
🔨 Latest commit d6bf931
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/6a202ad0c4eec50008e3b542
😎 Deploy Preview https://deploy-preview-11434.dashboard-v2.novu-staging.co
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR introduces useBootstrapOrganization, a hook that polls Novu's environments endpoint until Clerk's externalOrgId syncs, then triggers Clerk reloads. This hook is integrated into EnvironmentProvider, and two pages—CLI auth and agents setup—are refactored to rely on simplified environment loading state and organization presence rather than component-local polling.

Changes

Organization Bootstrap Flow

Layer / File(s) Summary
Bootstrap organization hook
apps/dashboard/src/context/environment/use-bootstrap-organization.ts
New hook implements needsBootstrap detection when Clerk org is loaded but _id is empty, conditionally polls Novu environments via useFetchEnvironments with 1s interval, and triggers one-time clerkUser.reload() and clerkOrganization.reload() when environments become available.
Environment provider bootstrap integration
apps/dashboard/src/context/environment/environment-provider.tsx
Imports and invokes useBootstrapOrganization() within EnvironmentProvider to coordinate bootstrap before environment queries activate.
CLI auth authorization loading state
apps/dashboard/src/pages/cli-auth.tsx
CliAuthContent consumes areEnvironmentsInitialLoading and derives isAuthorizeDataLoading combining environment initialization, API key loading, and missing currentEnvironment. Authorization reason returns null while loading, and Authorize button disabled/isLoading reflects isAuthorizeDataLoading.
Agents setup data readiness simplification
apps/dashboard/src/pages/agents-setup-page.tsx
Removes component-local useAgentEnvLoading hook and environment polling. Data readiness now depends solely on currentOrganization?._id and currentEnvironment. Clerk user/organization reload logic removed. Render gating simplified to return null when provisioning is active, show loader when data is not ready, then render onboarding UI.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • novuhq/novu#11301: Establishes onboarding provisioning infrastructure and modifies agents-setup-page.tsx, which this PR builds upon by refactoring to rely on the bootstrap mechanism for environment readiness.
  • novuhq/novu#11326: Implements CLI auth session persistence and resumption across sign-in and org setup flows, directly modifying cli-auth.tsx alongside this PR's environment loading refactor.
  • novuhq/novu#11331: Persists CLI auth session state through organization picker redirects, complementing this PR's organization bootstrap flow for the CLI authentication experience.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits format (type: fix, scope: dashboard) and uses imperative mood with lowercase description. It clearly summarizes the main change: polling Clerk until externalOrgId syncs on the CLI auth page.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

Hey there and thank you for opening this pull request! 👋

We require pull request titles to follow specific formatting rules and it looks like your proposed title needs to be adjusted.

Your PR title is: fix(dashboard): poll Clerk until externalOrgId sync on CLI auth page

Requirements:

  1. Follow the Conventional Commits specification
  2. As a team member, include Linear ticket ID at the end: fixes TICKET-ID or include it in your branch name

Expected format: feat(scope): Add fancy new feature fixes NOV-123

Details:

PR title must end with 'fixes TICKET-ID' (e.g., 'fixes NOV-123') or include ticket ID in branch name

Comment thread apps/dashboard/src/pages/cli-auth.tsx Outdated
Comment on lines +40 to +46
attemptsRef.current += 1;

if (attemptsRef.current > MAX_ATTEMPTS) {
setSyncTimedOut(true);

return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Attempt counter increments before the first reload fires

attemptsRef.current is incremented at the top of poll(), so the first call costs one attempt before any network request is made. When MAX_ATTEMPTS = 40, the timeout fires on the 41st increment — but only 40 calls to organization.reload() were ever made. This is off-by-one relative to the documented "40 attempts" budget. Consider checking >= MAX_ATTEMPTS instead of > MAX_ATTEMPTS, or moving the increment after the guard, to keep the semantics consistent.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/src/hooks/use-sync-clerk-external-org-id.ts
Line: 40-46

Comment:
**Attempt counter increments before the first reload fires**

`attemptsRef.current` is incremented at the top of `poll()`, so the first call costs one attempt before any network request is made. When `MAX_ATTEMPTS = 40`, the timeout fires on the 41st increment — but only 40 calls to `organization.reload()` were ever made. This is off-by-one relative to the documented "40 attempts" budget. Consider checking `>= MAX_ATTEMPTS` instead of `> MAX_ATTEMPTS`, or moving the increment after the guard, to keep the semantics consistent.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor

Comment thread apps/dashboard/src/hooks/use-sync-clerk-external-org-id.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/dashboard/src/pages/cli-auth.tsx (1)

176-189: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix reason check ordering in cli-auth to avoid permission error during org sync/loading

useHasPermission() returns a function that yields false while Clerk useAuth().isLoaded is not ready (and/or permissions aren’t available yet). Because apps/dashboard/src/pages/cli-auth.tsx evaluates !canReadApiKeys (line ~181) before the syncTimedOut and isLoading checks (where isLoading includes isSyncingOrg), the page can show the api_key:read permission message instead of staying in the loading/timeout flow.

🔀 Suggested reordering
   const reason = (() => {
     if (!deviceCodeOk) return 'This page must be opened from the Novu CLI.';
     if (!isConnect && !isLlmGatewayEnabled) {
       return `${callerDisplayName} is not enabled for your account yet.`;
     }
-    if (!canReadApiKeys) return 'You need the api_key:read permission to authorize the CLI.';
     if (syncTimedOut) {
       return 'Your workspace is still being set up. Please refresh the page in a moment.';
     }
     if (isLoading) return null;
+    if (!canReadApiKeys) return 'You need the api_key:read permission to authorize the CLI.';
     if (!apiKey) return 'No API key is available in this environment.';

     return null;
   })();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard/src/pages/cli-auth.tsx` around lines 176 - 189, The computed
reason logic in the cli-auth page evaluates !canReadApiKeys (from
useHasPermission()) before checking syncTimedOut and isLoading, causing a
premature permission error while Clerk auth/permissions are still loading;
update the order inside the reason IIFE so that checks for deviceCodeOk remain
first, then syncTimedOut and isLoading (which cover isSyncingOrg/Clerk loading)
are evaluated before !canReadApiKeys, and keep the final apiKey presence and
default null checks unchanged to ensure the loading/sync flow displays prior to
any permission error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/dashboard/src/pages/cli-auth.tsx`:
- Around line 176-189: The computed reason logic in the cli-auth page evaluates
!canReadApiKeys (from useHasPermission()) before checking syncTimedOut and
isLoading, causing a premature permission error while Clerk auth/permissions are
still loading; update the order inside the reason IIFE so that checks for
deviceCodeOk remain first, then syncTimedOut and isLoading (which cover
isSyncingOrg/Clerk loading) are evaluated before !canReadApiKeys, and keep the
final apiKey presence and default null checks unchanged to ensure the
loading/sync flow displays prior to any permission error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a435da76-885d-4242-8d4d-464571de31cb

📥 Commits

Reviewing files that changed from the base of the PR and between 89e43bb and 75ddba4.

📒 Files selected for processing (2)
  • apps/dashboard/src/hooks/use-sync-clerk-external-org-id.ts
  • apps/dashboard/src/pages/cli-auth.tsx

Move externalOrgId polling into the environment layer, fix CLI auth
timeout UX so the Authorize button stops spinning when sync fails.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/dashboard/src/context/environment/use-sync-clerk-external-org-id.ts (1)

36-72: ⚡ Quick win

Polling timeout isn’t blocked by Clerk reload(); ref-based change is robustness

  • In Clerk v6.6.6, organization.reload() mutates the existing OrganizationResource instance in place and resolves with the same instance, so it shouldn’t reset the polling loop by itself; setSyncTimedOut(true) should still be reachable.
  • Keeping organization in the effect deps can still cause unnecessary poll restarts if the organization reference ever gets replaced (e.g., org switching). Using a ref and keying the effect off needsSync only would make the timeout behavior more robust.
🛠️ Proposed ref-based fix
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
@@
   const needsSync = Boolean(
     isOrgLoaded && isOrganizationLoaded && organization && !externalOrgId
   );

+  const organizationRef = useRef(organization);
+  organizationRef.current = organization;
+
   useEffect(() => {
     if (!needsSync) {
       setSyncTimedOut(false);

       return;
     }

     let cancelled = false;

     async function pollUntilSynced() {
       for (let attempt = 0; attempt < MAX_ATTEMPTS && !cancelled; attempt += 1) {
         try {
-          const refreshed = await organization?.reload();
+          const refreshed = await organizationRef.current?.reload();

-          if (readExternalOrgId(refreshed ?? organization)) {
+          if (readExternalOrgId(refreshed ?? organizationRef.current)) {
             return;
           }
         } catch {
           // Webhook may still be in flight; keep polling.
         }

         if (attempt < MAX_ATTEMPTS - 1) {
           await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
         }
       }

       if (!cancelled) {
         setSyncTimedOut(true);
       }
     }

     void pollUntilSynced();

     return () => {
       cancelled = true;
     };
-  }, [needsSync, organization]);
+  }, [needsSync]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard/src/context/environment/use-sync-clerk-external-org-id.ts`
around lines 36 - 72, The effect currently depends on organization which can
restart polling when the Clerks OrganizationResource reference changes; instead,
keep the effect keyed only on needsSync and read the latest organization via a
ref: create a ref (e.g., organizationRef) that you update whenever organization
changes, use organizationRef.current inside pollUntilSynced (and when calling
organizationRef.current.reload()), and change the useEffect dependency array to
[needsSync] so pollUntilSynced runs only when needsSync changes; preserve the
existing cancelled flag, MAX_ATTEMPTS/POLL_INTERVAL_MS logic, and
setSyncTimedOut(true) behavior for the timeout case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/dashboard/src/pages/cli-auth.tsx`:
- Around line 186-199: The permission check for canReadApiKeys is happening too
early in the reason IIFE and can show a misleading "api_key:read" message while
auth/RBAC or org sync is still loading; reorder the guards inside the reason
function so that deviceCodeOk remains first, then handle
organizationSyncTimedOut and isAuthorizeDataLoading (or any loading/sync guards)
before checking canReadApiKeys, and keep the final apiKey presence check and the
default null return; update references to reason, canReadApiKeys,
organizationSyncTimedOut, isAuthorizeDataLoading, deviceCodeOk, and apiKey
accordingly.

---

Nitpick comments:
In `@apps/dashboard/src/context/environment/use-sync-clerk-external-org-id.ts`:
- Around line 36-72: The effect currently depends on organization which can
restart polling when the Clerks OrganizationResource reference changes; instead,
keep the effect keyed only on needsSync and read the latest organization via a
ref: create a ref (e.g., organizationRef) that you update whenever organization
changes, use organizationRef.current inside pollUntilSynced (and when calling
organizationRef.current.reload()), and change the useEffect dependency array to
[needsSync] so pollUntilSynced runs only when needsSync changes; preserve the
existing cancelled flag, MAX_ATTEMPTS/POLL_INTERVAL_MS logic, and
setSyncTimedOut(true) behavior for the timeout case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cb719a12-90a0-4a63-a2bb-e05d7715737d

📥 Commits

Reviewing files that changed from the base of the PR and between 75ddba4 and 0f11bb6.

📒 Files selected for processing (4)
  • apps/dashboard/src/context/environment/environment-context.tsx
  • apps/dashboard/src/context/environment/environment-provider.tsx
  • apps/dashboard/src/context/environment/use-sync-clerk-external-org-id.ts
  • apps/dashboard/src/pages/cli-auth.tsx

Comment thread apps/dashboard/src/pages/cli-auth.tsx
@scopsy
Copy link
Copy Markdown
Contributor Author

scopsy commented Jun 3, 2026

@cursoragent review comments

@cursor
Copy link
Copy Markdown
Contributor

cursor Bot commented Jun 3, 2026

I triaged the review comments and landed fixes for the three that were still valid.

Fixed (3)

  1. agents-setup-page.tsx — infinite spinner on org sync timeout
    Removed organizationSyncTimedOut from isEnvironmentLoading and added an early return that shows the same “workspace still being set up” message as CLI auth, plus a Refresh page button.

  2. cli-auth.tsx — misleading permission message during loading
    Reordered the reason guards so org sync timeout and loading checks run before the canReadApiKeys check, avoiding a false “api_key:read” error while RBAC is still loading.

  3. use-sync-clerk-external-org-id.ts — polling interval reset on reload
    Read organization through a ref so the effect deps stay stable and the 1.5s backoff isn’t cancelled on every organization.reload().

Skipped / stale (3)

  1. CLI auth isLoading spinner during timeout — already handled via isAuthorizeDataLoading (!organizationSyncTimedOut && ...).
  2. Off-by-one attempt counter — already fixed; the hook uses a for loop with exactly MAX_ATTEMPTS reload calls.
  3. Greptile’s agents-setup infinite spinner — was still valid before this commit; now fixed.

Changes are pushed to fix/dashboard-cli-auth-org-sync (commit 1392833070) on PR #11434.

Open in Web Open in Cursor 

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/dashboard/src/pages/agents-setup-page.tsx`:
- Around line 190-195: The current loading flag folds organizationSyncTimedOut
into isEnvironmentLoading causing the page to render OnboardingLoader forever
when the Clerk sync times out; refactor so organizationSyncTimedOut is handled
as its own UI state instead of part of isEnvironmentLoading: remove
organizationSyncTimedOut from the isEnvironmentLoading expression (which
currently combines isOrganizationSyncing, organizationSyncTimedOut,
areEnvironmentsInitialLoading, and Boolean(organizationId &&
!isEnvironmentReady)), keep isDataReady = isEnvironmentReady, and add a separate
conditional branch that checks organizationSyncTimedOut (render an explicit
error/timeout UI or retry prompt) before falling back to the normal loading
(OnboardingLoader) or data-ready paths; update any other similar checks around
lines 305-310 that mix organizationSyncTimedOut into loading flags to follow the
same pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a698cdf8-84de-4f80-97ac-ebd48f12f99b

📥 Commits

Reviewing files that changed from the base of the PR and between 0f11bb6 and 891b566.

📒 Files selected for processing (1)
  • apps/dashboard/src/pages/agents-setup-page.tsx

Comment thread apps/dashboard/src/pages/agents-setup-page.tsx Outdated
- Show refresh message on agents-setup when Clerk org sync times out
- Reorder CLI auth guards so loading state precedes permission checks
- Stabilize org sync polling with a ref to preserve poll interval

Co-authored-by: Dima Grossman <dima@grossman.io>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/dashboard/src/pages/agents-setup-page.tsx (1)

300-316: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Timeout UI is still masked by the provisioning gate.

Lines 300-302 return null before the new timeout branch can render, and this file only dismisses provisioning when isDataReady becomes true (Lines 196-199). On the Clerk-sync timeout path, isDataReady never flips, so the refresh message can stay unreachable and the user remains stuck behind the provisioning screen.

🔧 Minimal fix
-  if (provisioningActive) {
-    return null;
-  }
-
   if (organizationSyncTimedOut) {
     return (
       <div className="flex h-screen w-full flex-col items-center justify-center gap-4 px-4">
         <PageMeta title={isConnectHost ? 'Build and distribute agents' : pageTitle} />
         <p className="text-text-sub text-label-sm max-w-md text-center">
           Your workspace is still being set up. Please refresh the page in a moment.
         </p>
         <Button variant="secondary" mode="outline" size="xs" onClick={() => window.location.reload()}>
           Refresh page
         </Button>
       </div>
     );
   }
+
+  if (provisioningActive) {
+    return null;
+  }

As per coding guidelines, "apps/dashboard/**: Check for proper loading/error states."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard/src/pages/agents-setup-page.tsx` around lines 300 - 316, The
provisioningActive early-return prevents the organizationSyncTimedOut branch
from ever being shown; modify the control flow in the component to allow the
timeout UI to render even when provisioningActive is true by checking
organizationSyncTimedOut before returning null for provisioningActive (or by
augmenting the provisioningActive condition to short-circuit if
organizationSyncTimedOut is true). Update the logic that uses
provisioningActive, organizationSyncTimedOut, and isDataReady so that the
organizationSyncTimedOut branch can be reached (e.g., move the
organizationSyncTimedOut block above the provisioningActive return or change the
provisioningActive guard to: if (provisioningActive &&
!organizationSyncTimedOut) return null).
apps/dashboard/src/pages/cli-auth.tsx (1)

179-199: ⚠️ Potential issue | 🔴 Critical

Fix CLI auth empty-environments recovery: add polling like AgentsSetupPage

cli-auth relies on EnvironmentProvider’s useFetchEnvironments({ organizationId, showError: false }) with no refetchInterval, so if the first fetch returns environments: [], isAuthorizeDataLoading will stay true because !currentEnvironment never becomes truthy. In contrast, agents-setup-page explicitly polls when !environments?.length via refetchInterval: 1000.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard/src/pages/cli-auth.tsx` around lines 179 - 199, The page's
auth logic (isAuthorizeDataLoading and reason) can hang when environments
initially return empty because useFetchEnvironments is not polling; update the
EnvironmentProvider/useFetchEnvironments call used by the cli-auth page to add
the same polling behavior as agents-setup-page (e.g., supply refetchInterval:
1000 or conditionally enable refetch when !environments?.length) so that
currentEnvironment can become available and isAuthorizeDataLoading can resolve;
locate the useFetchEnvironments invocation referenced by cli-auth and mirror the
agents-setup-page polling strategy to fix the empty-environments recovery.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/dashboard/src/pages/agents-setup-page.tsx`:
- Around line 300-316: The provisioningActive early-return prevents the
organizationSyncTimedOut branch from ever being shown; modify the control flow
in the component to allow the timeout UI to render even when provisioningActive
is true by checking organizationSyncTimedOut before returning null for
provisioningActive (or by augmenting the provisioningActive condition to
short-circuit if organizationSyncTimedOut is true). Update the logic that uses
provisioningActive, organizationSyncTimedOut, and isDataReady so that the
organizationSyncTimedOut branch can be reached (e.g., move the
organizationSyncTimedOut block above the provisioningActive return or change the
provisioningActive guard to: if (provisioningActive &&
!organizationSyncTimedOut) return null).

In `@apps/dashboard/src/pages/cli-auth.tsx`:
- Around line 179-199: The page's auth logic (isAuthorizeDataLoading and reason)
can hang when environments initially return empty because useFetchEnvironments
is not polling; update the EnvironmentProvider/useFetchEnvironments call used by
the cli-auth page to add the same polling behavior as agents-setup-page (e.g.,
supply refetchInterval: 1000 or conditionally enable refetch when
!environments?.length) so that currentEnvironment can become available and
isAuthorizeDataLoading can resolve; locate the useFetchEnvironments invocation
referenced by cli-auth and mirror the agents-setup-page polling strategy to fix
the empty-environments recovery.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4c4e0c73-4264-4274-8345-0709f4cc3dc4

📥 Commits

Reviewing files that changed from the base of the PR and between 891b566 and 1392833.

📒 Files selected for processing (3)
  • apps/dashboard/src/context/environment/use-sync-clerk-external-org-id.ts
  • apps/dashboard/src/pages/agents-setup-page.tsx
  • apps/dashboard/src/pages/cli-auth.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/dashboard/src/context/environment/use-sync-clerk-external-org-id.ts

Replace the Clerk-reload polling with a Novu environments poll in
EnvironmentProvider (the inbox onboarding pattern), reloading Clerk once
when the org is ready. Fixes the agents setup page stuck on an infinite
loader and simplifies CLI auth/agents readiness to the Novu org id.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/dashboard/src/context/environment/use-bootstrap-organization.ts`:
- Around line 22-48: The bootstrap polling loop in use-bootstrap-organization.ts
(driven by needsBootstrap, useFetchEnvironments with BOOTSTRAP_ORG_CACHE_KEY and
refetchInterval) has no timeout/max-attempts so it can poll forever; add a
bounded retry policy (either maxAttempts counter or elapsed-time timeout)
tracked in the hook (e.g., a ref like hasReloadedRef but for attempts/startedAt)
that stops polling (clear or change refetchInterval) after the limit, surface a
timedOut boolean from the hook API, and set hasReloadedRef/current reload logic
to only attempt clerkUser.reload() and clerkOrganization.reload() a limited
number of times; ensure consumers (those reading
areEnvironmentsInitialLoading/isDataReady) can read the new timedOut flag to
render an error/refresh state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c1fec481-7f60-4f17-9e74-f8abfb616285

📥 Commits

Reviewing files that changed from the base of the PR and between 1392833 and d6bf931.

📒 Files selected for processing (4)
  • apps/dashboard/src/context/environment/environment-provider.tsx
  • apps/dashboard/src/context/environment/use-bootstrap-organization.ts
  • apps/dashboard/src/pages/agents-setup-page.tsx
  • apps/dashboard/src/pages/cli-auth.tsx

Comment on lines +22 to +48
const needsBootstrap = Boolean(
isClerkOrgLoaded && isOrganizationLoaded && clerkOrganization && !currentOrganization?._id
);

const { environments } = useFetchEnvironments({
organizationId: needsBootstrap ? BOOTSTRAP_ORG_CACHE_KEY : '',
refetchInterval: needsBootstrap ? 1000 : undefined,
showError: false,
});

const hasReloadedRef = useRef(false);

useEffect(() => {
if (!needsBootstrap) {
hasReloadedRef.current = false;

return;
}

if (!environments?.length || hasReloadedRef.current) {
return;
}

hasReloadedRef.current = true;
void clerkUser?.reload();
void clerkOrganization?.reload();
}, [needsBootstrap, environments, clerkUser, clerkOrganization]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

No timeout/max-attempts: a stalled org sync polls forever and strands consumers on a spinner.

The bootstrap loop has no upper bound. If Novu never reports the org as ready (backend fails to write externalOrgId, or clerkOrganization.reload() doesn't sync it), needsBootstrap stays true and the query keeps polling every 1s indefinitely. Because hasReloadedRef only allows a single reload, there's no recovery path and no way to surface an error.

This regresses the previous organizationSyncTimedOut behavior. Downstream this leaves areEnvironmentsInitialLoading/isDataReady perpetually unresolved, so the CLI auth Authorize button (cli-auth.tsx Line 282) and the agents-setup loader (agents-setup-page.tsx Lines 241-248) spin forever with no timeout message or refresh action.

Consider re-introducing a bounded attempt count / elapsed-time guard and exposing a timed-out signal so consumers can render an error/refresh state.

As per coding guidelines: "apps/dashboard/**: Check for proper loading/error states."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard/src/context/environment/use-bootstrap-organization.ts` around
lines 22 - 48, The bootstrap polling loop in use-bootstrap-organization.ts
(driven by needsBootstrap, useFetchEnvironments with BOOTSTRAP_ORG_CACHE_KEY and
refetchInterval) has no timeout/max-attempts so it can poll forever; add a
bounded retry policy (either maxAttempts counter or elapsed-time timeout)
tracked in the hook (e.g., a ref like hasReloadedRef but for attempts/startedAt)
that stops polling (clear or change refetchInterval) after the limit, surface a
timedOut boolean from the hook API, and set hasReloadedRef/current reload logic
to only attempt clerkUser.reload() and clerkOrganization.reload() a limited
number of times; ensure consumers (those reading
areEnvironmentsInitialLoading/isDataReady) can read the new timedOut flag to
render an error/refresh state.

@scopsy scopsy merged commit c3ece9f into next Jun 3, 2026
37 of 38 checks passed
@scopsy scopsy deleted the fix/dashboard-cli-auth-org-sync branch June 3, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants