Skip to content

Onboarding dialog stuck: 'Get started' button does nothing after invitation sign-in via Google #384

Closed

Description

@rosalie1984

Description

When onboarding a new user via an email invitation sent to a private (non-organization) email address, the final Get started button on the onboarding dialog does nothing — the user remains stuck on the dialog and never reaches the portal.

Steps to reproduce

  1. Invite a user via their private email address.
  2. User receives the invitation email and follows the link.
  3. User signs in via Google (sign-in itself works).
  4. The onboarding dialog appears; user fills in all requested fields.
  5. User clicks Get started.

Expected behavior

Onboarding completes and the user is taken into the portal.

Actual behavior

Nothing visibly happens. No error message, no navigation — the onboarding dialog stays open and the user is stuck. Retrying the button has no effect.

Notes / possible avenue

The symptom ('button does nothing', no feedback) matches the known hub-deadlock pattern documented in CLAUDE.md: an await on a hub-backed service inside a click action or hub handler instead of the reactive IObservable + Subscribe pattern. Worth checking the onboarding dialog's Get-started click handler and the user-provisioning service it calls (mesh node creation for the new user) for await/.ToTask()/FirstAsync() usage.

Also worth verifying whether the failure is specific to invitation-based accounts with external (Google) identities — e.g. the user-provisioning write failing silently due to missing access context for a not-yet-provisioned user.

Reported by @rsalzmann (reproduced with a private email + Google sign-in).

Activity

  1. rbuergi commented on Jul 18, 2026

    @rbuergi
    Contributor

    RCA (verified against source)

    Primary root cause — already fixed before this issue was filed. The submit path works (Onboarding.razor:295 HandleSubmit → the 4-query CombineLatest → UserOnboardingService.CreateUser/GrantSelfAdmin → OnOnboardingSucceeded forced reload). The failure was that on the forced reload OnboardingMiddleware.FindUserByEmail's workspace.GetQuery("auth:userByEmail:{email}") replays a permanently-cached EMPTY snapshot (pg_notify disabled on the partitioned portal → GetQuery is Replay(1) with no live delta), so the just-onboarded user is bounced back to /onboarding ("button did nothing / cannot advance"). The authoritative read-after-write recovery (QueryUserByEmailAuthoritative, OnboardingMiddleware.cs:306) shipped in PR #319, merged 2026-07-06 — three days before this issue. So this was most likely filed against a deployed image predating #319; current main has the fix.

    Residual (Google-specific, actionable): Google has no preferred_username claim, so ObjectId = the numeric sub; all onboarding lookups instead key on Email. The recovery works only if the cookie's email exactly matches the stored content.email — an external/Google address whose casing differs (not normalized the way org UPNs are) would defeat the re-read and reproduce the exact loop, for external emails only. Fix (hardening, PR incoming): normalize email to a canonical trimmed/lower-cased form symmetrically at the write (content.email) and every read (FindUserByEmail, the Onboarding queries) so the post-onboarding lookup is casing-robust. NOT a retry/timer (that's the forbidden band-aid class — the authoritative re-read is the correct mechanism). Primary verification is redeploy main + confirm.

  2. rbuergi commented on Jul 18, 2026

    @rbuergi
    Contributor

    Correction to my earlier comment: I hypothesized a residual Google/external-email casing gap, but on tracing every onboarding write/read of content.email (and an empirical revert-check on current main) that gap does not reproduce — the lookup is case-insensitive on both backends:

    • Postgres: content.email is a text field → PostgreSqlSqlGenerator emits LOWER(content->>'email') = @p with @p = value.ToLowerInvariant() (PostgreSqlSqlGenerator.cs:1005-1008).
    • In-memory: QueryEvaluator.CompareEqual uses OrdinalIgnoreCase (QueryEvaluator.cs:207).

    A probe storing Mixed.Case@Example.COM and looking up mixed.case@example.com via the exact FindUserByEmail query shape passed on main with zero code change. Also, the onboarding email field is read-only for any @-address (Onboarding.razor:157), so the stored and looked-up emails derive from the same cookie claim in one session (byte-identical anyway).

    Net: there is no in-repo code fix needed. The primary bounce-loop was fixed by #319 (present on main); redeploy main and verify on the live portal clears any lingering stale-cache instances. No PR — shipping symmetric normalization would be a no-op that can't be revert-proven.

  3. rbuergi commented on Jul 18, 2026

    @rbuergi
    Contributor

    No code change required. The primary cause (stale synced-query cache bouncing the just-onboarded user back to /onboarding) was already fixed by #319's authoritative read-after-write, present on main — so this needs a redeploy of main + verify on the live portal to clear. The hypothesized Google/external-email casing residual was empirically disproved (the lookup is case-insensitive on both backends; a mixed-case probe resolves the user on main with zero code change — see the correction comment above). Closing as fixed-in-code; reopen if it still reproduces after a redeploy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions