From db57cfd70b80915a4073af4ebf04633b5f40a7f5 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Sat, 20 Jun 2026 02:13:57 -0700 Subject: [PATCH] feat(web): gate team onboarding behind company-brain-beta flag --- apps/web/app/(app)/onboarding/page.tsx | 32 ++++++++-- .../web/components/onboarding-brain/shell.tsx | 5 +- .../onboarding-brain/step-about.tsx | 63 ++++++++++++++++++- apps/web/hooks/use-feature-flag.ts | 14 +++++ 4 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 apps/web/hooks/use-feature-flag.ts diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx index 71d54161..cb24ab5b 100644 --- a/apps/web/app/(app)/onboarding/page.tsx +++ b/apps/web/app/(app)/onboarding/page.tsx @@ -15,6 +15,7 @@ import { type SourcesValues, } from "@/components/onboarding-brain/step-sources" import { StepIngest } from "@/components/onboarding-brain/step-ingest" +import { useFeatureFlag } from "@/hooks/use-feature-flag" import { StepTeam, type TeamValues, @@ -64,6 +65,8 @@ export default function BrainOnboardingPage() { [user?.email], ) + // Team (Company Brain) onboarding is gated behind a private-beta flag. + const allowTeam = useFeatureFlag("company-brain-beta") const [mode, setMode] = useState(detectedMode) const [about, setAbout] = useState({ name: user?.name ?? "", @@ -132,6 +135,13 @@ export default function BrainOnboardingPage() { return plan === "scale" || plan === "scale_yearly" }, [org]) + // Personal onboarding has no team step — drop it from the flow + stepper. + const steps = useMemo( + () => + mode === "team" ? BRAIN_STEPS : BRAIN_STEPS.filter((s) => s !== "team"), + [mode], + ) + const finish = useCallback(async () => { try { localStorage.removeItem(STORAGE_KEY) @@ -140,14 +150,22 @@ export default function BrainOnboardingPage() { }, [router]) const goNext = useCallback(() => { - const idx = BRAIN_STEPS.indexOf(step) - const next = BRAIN_STEPS[idx + 1] + const idx = steps.indexOf(step) + const next = steps[idx + 1] if (!next) { finish() return } setStepAndUrl(next) - }, [step, setStepAndUrl, finish]) + }, [step, steps, setStepAndUrl, finish]) + + // If the current step isn't valid for the mode (e.g. switched to personal), + // fall back to the last valid step. + useEffect(() => { + if (!steps.includes(step)) { + setStepAndUrl(steps[steps.length - 1] ?? "about") + } + }, [steps, step, setStepAndUrl]) const [creatingOrg, setCreatingOrg] = useState(false) const creatingOrgRef = useRef(false) @@ -156,13 +174,14 @@ export default function BrainOnboardingPage() { if (!forceCreate && organizations && organizations.length > 0) return const name = (about.workspaceName || suggestedWorkspaceName).trim() const slug = generateOrgSlug(name) + const effectiveMode = allowTeam ? mode : "personal" const metadata: BrainMetadata & { signupSource: string } = { signupSource: "consumer", brainOnboardingVersion: "v1", - brainMode: mode, + brainMode: effectiveMode, brainWorkspaceName: name, brainWorkspaceDomain: - mode === "team" ? about.workspaceDomain || domain : null, + effectiveMode === "team" ? about.workspaceDomain || domain : null, brainContainerTag: containerTag, ...(about.about.trim() ? { brainAbout: about.about.trim() } : {}), } @@ -185,6 +204,7 @@ export default function BrainOnboardingPage() { about, suggestedWorkspaceName, mode, + allowTeam, domain, containerTag, setActiveOrg, @@ -261,12 +281,14 @@ export default function BrainOnboardingPage() { return ( {step === "about" && ( void + allowTeam: boolean domain: string | null suggestedWorkspaceName: string defaultName: string @@ -58,6 +60,7 @@ const inputClass = export function StepAbout({ mode, onModeChange, + allowTeam, domain, suggestedWorkspaceName, defaultName, @@ -80,8 +83,11 @@ export function StepAbout({ if (Object.keys(patch).length > 0) onChange({ ...values, ...patch }) }, [defaultName, suggestedWorkspaceName, domain]) + const teamGated = mode === "team" && !allowTeam const canContinue = - values.name.trim().length > 0 && values.workspaceName.trim().length > 0 + !teamGated && + values.name.trim().length > 0 && + values.workspaceName.trim().length > 0 return (
@@ -146,7 +152,9 @@ export function StepAbout({
- {mode === "team" ? ( + {teamGated ? ( + onModeChange("personal")} /> + ) : mode === "team" ? ( @@ -358,6 +366,57 @@ function PersonalWorkspaceCard({ ) } +function TeamBetaGate({ onUsePersonal }: { onUsePersonal: () => void }) { + return ( +
+
+

+ Private beta +

+

+ Team workspaces are invite-only +

+

+ We're onboarding teams to Company Brain one at a time. Email us for + access — or start with a personal workspace and invite your team later. +

+
+ + + support@supermemory.com + + + +
+
+ ) +} + function ModeToggle({ mode, onChange, diff --git a/apps/web/hooks/use-feature-flag.ts b/apps/web/hooks/use-feature-flag.ts new file mode 100644 index 00000000..7ece41c3 --- /dev/null +++ b/apps/web/hooks/use-feature-flag.ts @@ -0,0 +1,14 @@ +import posthog from "posthog-js" +import { useEffect, useState } from "react" + +// Reactive PostHog boolean flag via the global singleton (the app uses a custom +// provider, so posthog-js/react hooks have no context). Unloaded/undefined = false. +export function useFeatureFlag(key: string): boolean { + const [enabled, setEnabled] = useState(false) + useEffect(() => { + const sync = () => setEnabled(posthog.isFeatureEnabled?.(key) === true) + sync() + return posthog.onFeatureFlags(() => sync()) + }, [key]) + return enabled +}