feat(web): open company brain signup to everyone (#1342)

- Drop the company-brain-beta PostHog flag gate; Team mode is available to all users.
- Remove the invite-only gate card and the effectiveMode downgrade that silently wrote brainMode: "personal" on team signups.
- Add a "Use a personal workspace instead" link to the domain-confirm step, which had no way back once the gate stopped catching users.

Fixes ENG-1106
This commit is contained in:
MaheshtheDev 2026-07-22 21:05:53 +00:00
parent 72431e9afc
commit 3c3accaab9
3 changed files with 82 additions and 152 deletions

View file

@ -19,7 +19,6 @@ import {
} from "@/components/onboarding-brain/step-sources"
import { StepIngest } from "@/components/onboarding-brain/step-ingest"
import { CompanyBrainOnboarding } from "@/components/onboarding-brain/company-brain-onboarding"
import { useFeatureFlagEnabled } from "posthog-js/react"
import {
StepTeam,
type TeamValues,
@ -86,7 +85,6 @@ export default function BrainOnboardingPage() {
() => workspaceDomainFromEmail(user?.email),
[user?.email],
)
const allowTeam = useFeatureFlagEnabled("company-brain-beta") ?? false
const [mode, setMode] = useState<BrainMode>(detectedMode)
const [about, setAbout] = useState<AboutValues>({
name: user?.name ?? "",
@ -177,13 +175,13 @@ export default function BrainOnboardingPage() {
// provisioning creates (sm_org_shared) — not a workspace-name slug space.
const containerTag = useMemo(
() =>
allowTeam && mode === "team"
mode === "team"
? SHARED_TEAM_BRAIN_TAG
: containerTagFromWorkspace(
about.workspaceName || suggestedWorkspaceName,
mode,
),
[allowTeam, about.workspaceName, suggestedWorkspaceName, mode],
[about.workspaceName, suggestedWorkspaceName, mode],
)
const isScale = useMemo(() => {
@ -250,14 +248,13 @@ export default function BrainOnboardingPage() {
: about.workspaceName || suggestedWorkspaceName
).trim()
const slug = generateOrgSlug(name)
const effectiveMode = allowTeam ? mode : "personal"
const metadata: BrainMetadata & { signupSource: string } = {
signupSource: "consumer",
brainOnboardingVersion: "v1",
brainMode: effectiveMode,
brainMode: mode,
brainWorkspaceName: name,
brainWorkspaceDomain:
effectiveMode === "team"
mode === "team"
? domainOverride || about.workspaceDomain || domain
: null,
brainContainerTag: containerTag,
@ -303,7 +300,6 @@ export default function BrainOnboardingPage() {
about,
suggestedWorkspaceName,
mode,
allowTeam,
domain,
containerTag,
setActiveOrg,
@ -337,7 +333,7 @@ export default function BrainOnboardingPage() {
setCreatingOrg(false)
}
}, [ensureOrg, goNext, forceCreate, organizations, router])
const isCompanyBrain = allowTeam && mode === "team"
const isCompanyBrain = mode === "team"
const handleBrainConfirm = useCallback(
async (confirmedDomain: string): Promise<CompanyBrainConfirmResult> => {
@ -467,16 +463,18 @@ export default function BrainOnboardingPage() {
submitting={creatingOrg}
onConfirm={handleBrainConfirm}
onDone={finish}
onUsePersonal={() => {
analytics.onboardingModeSelected({ mode: "personal" })
setMode("personal")
setStepAndUrl("about")
}}
/>
)
}
// Team mode returns above, so everything below is the personal flow.
return (
<BrainShell
step={step}
steps={steps}
domain={mode === "team" ? about.workspaceDomain || domain : null}
>
<BrainShell step={step} steps={steps} domain={null}>
{step === "about" && (
<StepAbout
mode={mode}
@ -484,7 +482,6 @@ export default function BrainOnboardingPage() {
analytics.onboardingModeSelected({ mode: m })
setMode(m)
}}
allowTeam={allowTeam}
domain={domain}
suggestedWorkspaceName={suggestedWorkspaceName}
defaultName={user?.name ?? ""}
@ -505,11 +502,7 @@ export default function BrainOnboardingPage() {
/>
)}
{step === "ingest" && (
<StepIngest
mode={allowTeam ? mode : "personal"}
mcpUrl={mcpUrl}
onContinue={goNext}
/>
<StepIngest mode={mode} mcpUrl={mcpUrl} onContinue={goNext} />
)}
{step === "team" && (
<StepTeam

View file

@ -35,6 +35,7 @@ interface CompanyBrainOnboardingProps {
submitting: boolean
onConfirm: (domain: string) => Promise<CompanyBrainConfirmResult>
onDone: () => void
onUsePersonal: () => void
}
const BACKEND =
@ -67,6 +68,7 @@ export function CompanyBrainOnboarding({
submitting,
onConfirm,
onDone,
onUsePersonal,
}: CompanyBrainOnboardingProps) {
const [phase, setPhase] = useState<Phase>("confirm")
const [domain, setDomain] = useState(initialDomain)
@ -239,7 +241,15 @@ export function CompanyBrainOnboarding({
</motion.div>
{phase === "confirm" && (
<div className="w-full max-w-xl mx-auto mt-5 flex items-center justify-end px-1">
<div className="w-full max-w-xl mx-auto mt-5 flex items-center justify-between gap-4 px-1">
<button
type="button"
onClick={onUsePersonal}
disabled={submitting}
className="text-[13px] font-medium text-[#737373] transition-colors hover:text-[#fafafa] disabled:opacity-50"
>
Use a personal workspace instead
</button>
<Button
variant="insideOut"
onClick={handleConfirm}

View file

@ -11,7 +11,6 @@ import {
Building2,
LayoutGrid,
Loader2,
Mail,
Plug,
Terminal,
UserPlus,
@ -35,7 +34,6 @@ export interface AboutValues {
interface Props {
mode: BrainMode
onModeChange: (m: BrainMode) => void
allowTeam: boolean
domain: string | null
suggestedWorkspaceName: string
defaultName: string
@ -63,7 +61,6 @@ export const inputClass =
export function StepAbout({
mode,
onModeChange,
allowTeam,
domain,
suggestedWorkspaceName,
defaultName,
@ -86,8 +83,7 @@ export function StepAbout({
if (Object.keys(patch).length > 0) onChange({ ...values, ...patch })
}, [defaultName, suggestedWorkspaceName, domain])
const teamGated = mode === "team" && !allowTeam
const isTeam = mode === "team" && !teamGated
const isTeam = mode === "team"
// Default the workspace name per mode: name-derived for Personal (field hidden),
// email-derived for Team unless the user has typed their own.
@ -161,9 +157,7 @@ export function StepAbout({
}, [isTeam])
const canContinue =
!teamGated &&
values.name.trim().length > 0 &&
values.workspaceName.trim().length > 0
values.name.trim().length > 0 && values.workspaceName.trim().length > 0
return (
<div className="max-w-xl mx-auto space-y-5">
@ -270,83 +264,67 @@ export function StepAbout({
)}
</AnimatePresence>
{!teamGated && (
<div>
<div className="mb-2 flex items-center justify-between gap-2 pl-2">
<p className="font-semibold text-[14px] text-[#737373]">
{isTeam ? (
"What does your company do?"
) : (
<>
What are you here for?{" "}
<span className="text-[#525D6E] font-medium">
(optional)
</span>
</>
)}
</p>
{isTeam &&
(drafting ? (
<span className="inline-flex shrink-0 items-center gap-1 text-[11px] font-medium text-[#737373]">
<Loader2 className="size-3 animate-spin" />
Drafting from your site
<div>
<div className="mb-2 flex items-center justify-between gap-2 pl-2">
<p className="font-semibold text-[14px] text-[#737373]">
{isTeam ? (
"What does your company do?"
) : (
<>
What are you here for?{" "}
<span className="text-[#525D6E] font-medium">
(optional)
</span>
) : drafted ? (
<span className="inline-flex shrink-0 items-center gap-1 text-[11px] font-medium text-[#525D6E]">
<Wand2 className="size-3" />
Drafted from your site · edit anything
</span>
) : null)}
</div>
<Textarea
value={drafting ? "" : values.about}
onChange={(e) => {
aboutTouched.current = true
setDrafted(false)
onChange({ ...values, about: e.target.value })
}}
placeholder={
drafting
? ""
: isTeam
? "A sentence or two — what your company builds, who you serve, how the team works."
: "A sentence or two — what you do, what you're hoping the brain helps with."
}
rows={3}
disabled={drafting}
className={cn(
inputClass,
"h-auto resize-none py-3 leading-[1.5]",
drafting && "opacity-60",
</>
)}
style={inputBevelStyle}
/>
</p>
{isTeam &&
(drafting ? (
<span className="inline-flex shrink-0 items-center gap-1 text-[11px] font-medium text-[#737373]">
<Loader2 className="size-3 animate-spin" />
Drafting from your site
</span>
) : drafted ? (
<span className="inline-flex shrink-0 items-center gap-1 text-[11px] font-medium text-[#525D6E]">
<Wand2 className="size-3" />
Drafted from your site · edit anything
</span>
) : null)}
</div>
)}
<Textarea
value={drafting ? "" : values.about}
onChange={(e) => {
aboutTouched.current = true
setDrafted(false)
onChange({ ...values, about: e.target.value })
}}
placeholder={
drafting
? ""
: isTeam
? "A sentence or two — what your company builds, who you serve, how the team works."
: "A sentence or two — what you do, what you're hoping the brain helps with."
}
rows={3}
disabled={drafting}
className={cn(
inputClass,
"h-auto resize-none py-3 leading-[1.5]",
drafting && "opacity-60",
)}
style={inputBevelStyle}
/>
</div>
</div>
{teamGated ? (
<motion.div
key="gate"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.22, ease: "easeOut" }}
className="mt-6"
>
<TeamBetaGate onUsePersonal={() => onModeChange("personal")} />
</motion.div>
) : (
<motion.div
key={`perks-${mode}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
<PerksFooter
perks={mode === "team" ? TEAM_PERKS : PERSONAL_PERKS}
/>
</motion.div>
)}
<motion.div
key={`perks-${mode}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
<PerksFooter perks={mode === "team" ? TEAM_PERKS : PERSONAL_PERKS} />
</motion.div>
</section>
<div className="flex items-center justify-end gap-[22px] px-1">
@ -429,57 +407,6 @@ export function DomainLogo({ domain }: { domain: string }) {
)
}
function TeamBetaGate({ onUsePersonal }: { onUsePersonal: () => void }) {
return (
<div
className="relative overflow-hidden rounded-[14px] bg-[#1B1F24] p-5 md:p-6"
style={cardSurfaceStyle}
>
<div
aria-hidden
className="absolute -top-px left-0 right-0 h-px"
style={{
background:
"linear-gradient(to right, transparent, rgba(75,160,250,0.3), transparent)",
}}
/>
<p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-[#4BA0FA]">
Private beta
</p>
<p
className={cn(
"mt-1.5 text-[15px] font-semibold leading-snug text-[#fafafa]",
dmSans125ClassName(),
)}
>
Team workspaces are invite-only
</p>
<p className="mt-1.5 text-[13px] leading-relaxed text-[#737373]">
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.
</p>
<div className="mt-4 flex w-full flex-col items-center gap-2.5">
<a
href="mailto:support@supermemory.com?subject=Company%20Brain%20beta%20access"
className="group inline-flex items-center gap-2 rounded-[10px] border border-[rgba(82,89,102,0.2)] bg-[#14161A] px-3.5 py-2 text-[13px] font-medium text-[#fafafa] transition-colors hover:border-[rgba(75,160,250,0.4)]"
>
<Mail className="size-3.5 text-[#4BA0FA]" />
support@supermemory.com
<ArrowRight className="size-3.5 text-[#525D6E] transition-colors group-hover:text-[#fafafa]" />
</a>
<button
type="button"
onClick={onUsePersonal}
className="inline-flex items-center gap-1.5 text-[13px] font-medium text-[#737373] transition-colors hover:text-[#fafafa]"
>
Start with a personal workspace
<ArrowRight className="size-3.5" />
</button>
</div>
</div>
)
}
function ModeToggle({
mode,
onChange,