Skip to content

feat(web): port provider stack (Auth, Organization, Feature Flags) from platform repo#31099

Merged
ashleeradka merged 3 commits into
mainfrom
devin/1779164551-provider-stack
May 19, 2026
Merged

feat(web): port provider stack (Auth, Organization, Feature Flags) from platform repo#31099
ashleeradka merged 3 commits into
mainfrom
devin/1779164551-provider-stack

Conversation

@ashleeradka
Copy link
Copy Markdown
Contributor

@ashleeradka ashleeradka commented May 19, 2026

Prompt / plan

Port the foundational Auth, Organization, and Feature Flag providers from vellum-assistant-platform to the assistant repo. These are cross-domain shared utilities required by every domain (Chat, Settings, Library, Onboarding) — without them, every parallel domain migration session would independently stub useAuth(), useAppFeatureFlags(), and useOrganization(), creating inconsistency and merge conflicts.

Platform files frozen in migration table: PR #7198.

What's included

Module Files What it does
Auth lib/auth/allauth-client.ts, lib/auth/csrf.ts, lib/auth/auth-provider.tsx Allauth session adapter, CSRF token management, AuthProvider context (session probe on mount, re-validate on focus/visibility, cross-tab logout via BroadcastChannel)
Organization lib/organization/organization-state.ts, lib/organization/organization-provider.tsx Module-level org ID state (useSyncExternalStore-compatible), OrganizationProvider context (fetches orgs via React Query, resolves active org, syncs to request interceptor)
Feature Flags lib/feature-flags/feature-flag-provider.tsx Typed AppFeatureFlagProvider + useAppFeatureFlags() context with all current flags
API Interceptors lib/api-interceptors.ts Side-effect module that installs Vellum-Organization-Id header and X-CSRFToken on mutating requests for both auth and platform HeyAPI clients
AppProviders lib/providers/app-providers.tsx Composes Auth → Organization → scope-keyed QueryClient so switching users/orgs yields a fresh React Query cache

Changes from platform source

  • Removed: Native platform detection, biometric auth, push token registration, Sentry user context, onboarding sync — none of these apply to the SPA
  • Simplified: No SSR guards (typeof window, typeof document) — SPA always has DOM
  • Fixed: ProviderSignupContext.account typed correctly against generated ProviderAccount (platform had drift)
  • Upgraded: tsconfig lib from ES2022 → ES2023 for Array.findLast support in CSRF cookie parsing
  • Cleaned up: Removed dead query-client.ts (replaced by scope-keyed provider in AppProviders)

Alternatives not taken

  • Stub providers per domain: Would duplicate auth/org logic in every parallel session and create merge conflicts. Provider stack as a shared foundation avoids this.
  • Port feature flags as part of Chat domain: Feature flags are used across ~20 consumers in Chat, Settings, Library, and assistant components. Making them a Chat-specific concern would couple unrelated domains.
  • Keep single QueryClient: Platform scopes the QueryClient by (user, org) to prevent data leakage across account switches. Preserving this in the SPA via key prop remounting.

Test plan

  • bunx tsc --noEmit passes (zero type errors)
  • bun run lint passes (zero lint errors)
  • CI checks (lint, typecheck, tests)
  • No runtime testing needed — providers are consumed by domain components not yet ported. The dev server will exercise them once Chat or Settings land.

Link to Devin session: https://app.devin.ai/sessions/536ceadb023a4059908b0609b9833bc1
Requested by: @ashleeradka


Open in Devin Review

Add the foundational Auth, Organization, and Feature Flag providers
that every domain (Chat, Settings, Library, Onboarding) depends on.

- Auth provider (allauth-client, csrf, auth-provider): session probe
  on mount, re-validate on focus/visibility, cross-tab logout sync
  via BroadcastChannel
- Organization provider: resolves active org from sessionStorage,
  keeps module-level request state in sync for API interceptors
- Feature flag provider: typed context with all current feature flags
- API interceptors: attaches Vellum-Organization-Id header and
  X-CSRFToken on mutating requests to both auth and platform clients
- AppProviders: scope-keyed QueryClient (by user+org) so switching
  users/orgs yields fresh cache
- Updated main.tsx to compose the full provider tree
- Upgraded tsconfig lib to ES2023 for Array.findLast support
- Removed dead query-client.ts (replaced by scope-keyed provider)

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

The provider stack imports from generated HeyAPI clients which are
gitignored. CI needs to regenerate them from the committed OpenAPI
specs before typecheck and build can resolve the imports.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a927cc91e4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +11 to +12
import { client as authClient } from "@/generated/auth/client.gen.js";
import { client as platformClient } from "@/generated/api/client.gen.js";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add generation before importing gitignored HeyAPI clients

These imports make a fresh checkout depend on apps/web/src/generated/**, but that directory is gitignored (apps/web/.gitignore) and there are no tracked generated files (git ls-files apps/web/src/generated/** returns 0). I also checked the web PR workflow: after install it runs only lint, bun run typecheck, and build, with no bun run openapi-ts step, so CI and local bun run typecheck/bun run build will fail with missing @/generated/... modules unless the clients are generated as part of install/build/CI or committed despite the ignore.

Useful? React with 👍 / 👎.

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.

Fixed in 8a79627a4b — added bun run openapi-ts codegen step to pr-web.yaml before lint/typecheck/build. The OpenAPI specs are committed at apps/web/openapi-schemas/, so CI can regenerate the clients from them.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

vex-assistant-bot[bot]
vex-assistant-bot Bot previously approved these changes May 19, 2026
Copy link
Copy Markdown
Contributor

@vex-assistant-bot vex-assistant-bot Bot left a comment

Choose a reason for hiding this comment

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

APPROVE

Value: Every parallel domain migration session (Chat, Settings, Library) now has a shared, production-grade auth/org/feature-flag foundation to import from instead of independently stubbing useAuth() — eliminating the merge conflicts and drift that stub-per-domain would have caused.

What this does: Ports AuthProvider, OrganizationProvider, FeatureFlagProvider, the HeyAPI request interceptors (Vellum-Organization-Id + X-CSRFToken), and a scope-keyed AppProviders composition from vellum-assistant-platform → apps/web/src/lib/. Removes the dead query-client.ts. Adds a bun run openapi-ts codegen step to pr-web.yaml (CI was missing it; generated files are gitignored). Upgrades tsconfig.json to ES2023 for Array.findLast support in CSRF cookie parsing.


Codex P1 — resolved ✅
Codex flagged (on stale commit a927cc91) that @/generated/ imports would fail CI because the directory is gitignored and no codegen step existed. Devin fixed this in 8a79627a by adding bun run openapi-ts before lint/typecheck/build in pr-web.yaml. The OpenAPI specs are committed at apps/web/openapi-schemas/, so CI can always regenerate. Verified: pr-web.yaml diff adds the step at the correct position (post-install, pre-lint). ✅


Architecture observations:

organization-state.ts — module-level state with useSyncExternalStore-compatible subscription. Correct pattern per KB: external stores (module-level) use useSyncExternalStore to avoid tearing, not useEffect. subscribeToActiveOrganizationIdForRequests returns a stable unsubscribe function and moves subscribe/getSnapshot to module scope. ✅

app-providers.tsx — the two-layer QueryClient design is thoughtful: AuthScopedQueryClientProvider (keyed by user identity) wraps OrganizationProvider so org-list queries survive org switches; ScopeKeyedQueryClientProvider (keyed by user+org) wraps domain queries so the domain cache flushes on org switch. Data leakage across account switches is prevented. ✅

api-interceptors.ts — interceptors install at the HeyAPI client level, before globalThis.fetch is called. Capacitor/iOS safe: this is header injection, not SSE consumer wiring, so CapacitorHttp's fetch interception doesn't affect it. ✅

BroadcastChannel auth sync — correctly guarded with if (typeof BroadcastChannel === "undefined") return; and the cleanup closes the channel. ✅


Non-blocking observations:

  1. refreshOrganizations dep on full organizationsQuery objectuseCallback(async () => { await organizationsQuery.refetch(); }, [organizationsQuery]) uses the whole query object as a dep. React Query's useQuery returns a new object reference on every render (even when data hasn't changed), so refreshOrganizations is recreated on every render. Since it's in the useMemo deps for the context value, useOrganization() consumers re-render on every parent render cycle. Perf non-issue today (few consumers), but worth fixing before more components subscribe. Fix: destructure refetch first and dep on [refetch], which React Query guarantees is stable:

    const { refetch: refetchOrgs } = organizationsQuery;
    const refreshOrganizations = useCallback(async () => {
      await refetchOrgs();
    }, [refetchOrgs]);
  2. initSession missing cancelled guard — KB requires a cancelled flag in async useEffect callbacks (PR #6726 evidence). AuthProvider is root-level and won't unmount in practice, so this is harmless today. Leaving a note for if the hook shape ever changes.

  3. refreshSession() in focus/visibility handlers unguardedonFocus and onVisibilityChange call refreshSession() but don't .catch() the returned promise. If getSession() throws on a network failure, this becomes an unhandled rejection that Sentry would capture as noise. Trivial fix: refreshSession().catch(err => console.warn("Session refresh failed", err)).


Merge gate:

  • ✅ Vex approves at 8a79627a
  • ⏳ Devin reviewed at HEAD (COMMENTED — needs to post explicit "No Issues Found" or APPROVE for merge criteria)
  • ⏳ CI — confirm bun run openapi-ts step passes in the new workflow

Vellum Constitution — Yours: every domain migration session now shares a single, correct auth identity — no more per-session stubs that quietly diverge.

- Destructure refetch from organizationsQuery to stabilize the
  refreshOrganizations callback reference (React Query guarantees
  refetch is stable, but the full query object is recreated per render)
- Add cancelled guard to initSession useEffect to prevent state updates
  after unmount
- Wrap refreshSession calls in focus/visibility handlers with .catch()
  to prevent unhandled rejection noise on network failures

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

All 3 non-blocking observations fixed in 97d633a:

  1. refreshOrganizations stability — Destructured refetch from organizationsQuery and dep on [refetchOrganizations] instead of the full query object. React Query guarantees refetch is stable.

  2. initSession cancelled guard — Added let cancelled = false with cleanup return. State updates are now gated on !cancelled.

  3. refreshSession unhandled rejection — Wrapped focus/visibility handlers with .catch()console.warn so network failures don't produce unhandled rejection noise.

@ashleeradka ashleeradka merged commit 2915152 into main May 19, 2026
3 checks passed
@ashleeradka ashleeradka deleted the devin/1779164551-provider-stack branch May 19, 2026 04:38
ashleeradka added a commit that referenced this pull request May 19, 2026
The provider stack PR (#31099) introduced imports from generated HeyAPI
clients but only added the codegen step to pr-web.yaml. The main branch
CI workflow was missing it, causing type check failures on every push
to main.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant