feat(web): company brain entitlement helpers + team-brain defaults

hasCompanyBrain/getBrainMode/getSignupSource helpers, useHasCompanyBrain team fallback, space visibility field, SHARED_TEAM_BRAIN_TAG, and default-to-Team-Brain in the store.
This commit is contained in:
Mahesh Sanikommu 2026-06-17 17:23:16 -07:00
parent c829fd3811
commit ecbb048209
6 changed files with 75 additions and 7 deletions

View file

@ -1,7 +1,9 @@
import { useAuth } from "@lib/auth-context"
import { hasCompanyBrain } from "@/lib/billing-utils"
import { getBrainMode, hasCompanyBrain } from "@/lib/billing-utils"
export function useHasCompanyBrain(): boolean {
const { org } = useAuth()
return hasCompanyBrain(org?.metadata as Record<string, unknown> | undefined)
const metadata = org?.metadata as Record<string, unknown> | string | undefined
// Team-brain orgs use brain spaces even before the add-on webhook lands.
return hasCompanyBrain(metadata) || getBrainMode(metadata) === "team"
}

View file

@ -1,10 +1,21 @@
const COMPANY_BRAIN_PRODUCT_ID = "company_brain"
// Add-on resolved by product presence, not tier.
// better-auth returns org.metadata as a JSON string, so accept string or object.
export function hasCompanyBrain(
metadata: Record<string, unknown> | null | undefined,
metadataRaw: Record<string, unknown> | string | null | undefined,
): boolean {
if (!metadata) return false
if (!metadataRaw) return false
let metadata: Record<string, unknown>
if (typeof metadataRaw === "string") {
try {
metadata = JSON.parse(metadataRaw) as Record<string, unknown>
} catch {
return false
}
} else {
metadata = metadataRaw
}
const overrides = metadata.featureOverrides as
| Record<string, { allow?: boolean }>
| undefined
@ -16,6 +27,49 @@ export function hasCompanyBrain(
return activeProducts.includes(COMPANY_BRAIN_PRODUCT_ID)
}
// Origin of the org. Consumer (app.supermemory) orgs get company_brain attached,
// but the add-on lands async — signupSource is set at creation, so it's the
// reliable "this org uses brain spaces" signal in the UI.
export function getSignupSource(
metadataRaw: Record<string, unknown> | string | null | undefined,
): string | null {
if (!metadataRaw) return null
let metadata: Record<string, unknown>
if (typeof metadataRaw === "string") {
try {
metadata = JSON.parse(metadataRaw) as Record<string, unknown>
} catch {
return null
}
} else {
metadata = metadataRaw
}
return typeof metadata.signupSource === "string"
? (metadata.signupSource as string)
: null
}
// Brain mode chosen during onboarding ("personal" | "team"). Set synchronously
// at org creation, so it's the reliable pre-webhook signal for company brain.
export function getBrainMode(
metadataRaw: Record<string, unknown> | string | null | undefined,
): string | null {
if (!metadataRaw) return null
let metadata: Record<string, unknown>
if (typeof metadataRaw === "string") {
try {
metadata = JSON.parse(metadataRaw) as Record<string, unknown>
} catch {
return null
}
} else {
metadata = metadataRaw
}
return typeof metadata.brainMode === "string"
? (metadata.brainMode as string)
: null
}
/**
* Format a number with K/M suffix for display
* @example formatUsageNumber(1500000) => "1.5M"

View file

@ -3,18 +3,23 @@
import { useQueryState } from "nuqs"
import { projectParam } from "@/lib/search-params"
import { useCallback } from "react"
import { DEFAULT_PROJECT_ID } from "@lib/constants"
import { DEFAULT_PROJECT_ID, SHARED_TEAM_BRAIN_TAG } from "@lib/constants"
import { useHasCompanyBrain } from "@/hooks/use-company-brain"
export function useProject() {
const [selectedProjects, _setSelectedProjects] = useQueryState(
"project",
projectParam,
)
const hasCompanyBrain = useHasCompanyBrain()
const defaultTag = hasCompanyBrain
? SHARED_TEAM_BRAIN_TAG
: DEFAULT_PROJECT_ID
const selectedProject = selectedProjects[0] ?? DEFAULT_PROJECT_ID
const selectedProject = selectedProjects[0] ?? defaultTag
const effectiveContainerTags =
selectedProjects.length === 0 ? [DEFAULT_PROJECT_ID] : selectedProjects
selectedProjects.length === 0 ? [defaultTag] : selectedProjects
const setSelectedProjects = useCallback(
(projects: string[]) => {

View file

@ -1,5 +1,6 @@
const BIG_DIMENSIONS_NEW = 1536
const DEFAULT_PROJECT_ID = "sm_project_default"
const SHARED_TEAM_BRAIN_TAG = "sm_org_shared"
const SEARCH_MEMORY_SHORTCUT_URL =
"https://www.icloud.com/shortcuts/b0a132cc3c0d475196bc7014aa702a5c"
const ADD_MEMORY_SHORTCUT_URL =
@ -12,6 +13,7 @@ const POKE_RECIPE_URL = "https://supermemory.link/poke"
export {
BIG_DIMENSIONS_NEW,
DEFAULT_PROJECT_ID,
SHARED_TEAM_BRAIN_TAG,
SEARCH_MEMORY_SHORTCUT_URL,
ADD_MEMORY_SHORTCUT_URL,
RAYCAST_EXTENSION_URL,

View file

@ -6,6 +6,7 @@ export interface Project {
updatedAt: string
isExperimental?: boolean
emoji?: string
visibility?: "public" | "private" | "unlisted"
}
export interface ContainerTagListType extends Project {

View file

@ -1496,6 +1496,10 @@ export const ContainerTagListTypeSchema = z
description: "True if containerTag starts with 'sm_project_'",
example: true,
}),
visibility: z.enum(["public", "private", "unlisted"]).optional().openapi({
description: "Space visibility (company brain spaces)",
example: "public",
}),
})
.openapi({
description: