fix(dashboard): poll Clerk until externalOrgId sync on CLI auth page#11434
Conversation
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.
✅ Deploy preview added
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe PR introduces ChangesOrganization Bootstrap Flow
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ 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. Comment |
|
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: Requirements:
Expected format: Details: PR title must end with 'fixes TICKET-ID' (e.g., 'fixes NOV-123') or include ticket ID in branch name |
| attemptsRef.current += 1; | ||
|
|
||
| if (attemptsRef.current > MAX_ATTEMPTS) { | ||
| setSyncTimedOut(true); | ||
|
|
||
| return; | ||
| } |
There was a problem hiding this 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.
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.There was a problem hiding this comment.
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 winFix
reasoncheck ordering incli-authto avoid permission error during org sync/loading
useHasPermission()returns a function that yieldsfalsewhile ClerkuseAuth().isLoadedis not ready (and/or permissions aren’t available yet). Becauseapps/dashboard/src/pages/cli-auth.tsxevaluates!canReadApiKeys(line ~181) before thesyncTimedOutandisLoadingchecks (whereisLoadingincludesisSyncingOrg), 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
📒 Files selected for processing (2)
apps/dashboard/src/hooks/use-sync-clerk-external-org-id.tsapps/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.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/dashboard/src/context/environment/use-sync-clerk-external-org-id.ts (1)
36-72: ⚡ Quick winPolling timeout isn’t blocked by Clerk
reload(); ref-based change is robustness
- In Clerk v6.6.6,
organization.reload()mutates the existingOrganizationResourceinstance 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
organizationin the effect deps can still cause unnecessary poll restarts if theorganizationreference ever gets replaced (e.g., org switching). Using a ref and keying the effect offneedsSynconly 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
📒 Files selected for processing (4)
apps/dashboard/src/context/environment/environment-context.tsxapps/dashboard/src/context/environment/environment-provider.tsxapps/dashboard/src/context/environment/use-sync-clerk-external-org-id.tsapps/dashboard/src/pages/cli-auth.tsx
|
@cursoragent review comments |
|
I triaged the review comments and landed fixes for the three that were still valid. Fixed (3)
Skipped / stale (3)
Changes are pushed to |
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
apps/dashboard/src/pages/agents-setup-page.tsx
- 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>
There was a problem hiding this comment.
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 winTimeout UI is still masked by the provisioning gate.
Lines 300-302 return
nullbefore the new timeout branch can render, and this file only dismisses provisioning whenisDataReadybecomes true (Lines 196-199). On the Clerk-sync timeout path,isDataReadynever 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 | 🔴 CriticalFix CLI auth empty-environments recovery: add polling like AgentsSetupPage
cli-authrelies onEnvironmentProvider’suseFetchEnvironments({ organizationId, showError: false })with norefetchInterval, so if the first fetch returnsenvironments: [],isAuthorizeDataLoadingwill staytruebecause!currentEnvironmentnever becomes truthy. In contrast,agents-setup-pageexplicitly polls when!environments?.lengthviarefetchInterval: 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
📒 Files selected for processing (3)
apps/dashboard/src/context/environment/use-sync-clerk-external-org-id.tsapps/dashboard/src/pages/agents-setup-page.tsxapps/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.
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
apps/dashboard/src/context/environment/environment-provider.tsxapps/dashboard/src/context/environment/use-bootstrap-organization.tsapps/dashboard/src/pages/agents-setup-page.tsxapps/dashboard/src/pages/cli-auth.tsx
| 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]); |
There was a problem hiding this comment.
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.


Summary
/cli/auth) staying on a loading spinner after a new user signs up, creates an org, and returns from onboarding without a manual refresh.useSyncClerkExternalOrgIdto pollorganization.reload()until Novu'sexternalOrgIdappears in ClerkpublicMetadata, then environment and API key queries can run.Why
After org creation, the backend writes
externalOrgIdinto Clerk asynchronously. Until then,currentOrganization._idis empty,useFetchEnvironmentsstays disabled (staleTime: Infinity), andisLoadingnever 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: AuthorizeTest plan
npx novu@latest connectas a new user: sign up, create org, land on/cli/auth, Authorize button leaves loading without refresh.device_codestill 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 intoEnvironmentProviderto gate environment and API-key queries on organization readiness. Updatedcli-auth.tsxto derive loading state from environment initialization and added explicit timeout guards. Refactoredagents-setup-page.tsxto use the centralized bootstrap flow instead of component-local polling, simplifying data-readiness checks.Key technical decisions
EnvironmentProviderrather than repeatedly callingClerk.reload(), following the inbox onboarding pattern.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
externalOrgIdinto Clerk asynchronously, leavingcurrentOrganization._idempty and the environments query disabled until a full page refresh.useBootstrapOrganizationin the environment context: pollsgetEnvironmentsevery second (with a dummy cache key'org') and, once Novu confirms the org is ready (environments returned), callsclerkUser.reload()+clerkOrganization.reload()once to syncexternalOrgIdinto Clerk — replacing the previous, more fragile Clerk-side polling approach.AgentsSetupPage(which was doing the same thing inline) and wirescli-auth.tsxthroughareEnvironmentsInitialLoadingfrom 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
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: AuthorizePrompt To Fix All With AI
Reviews (4): Last reviewed commit: "fix(dashboard): bootstrap org via Novu e..." | Re-trigger Greptile