diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx
index 5fa70049..4ad8e215 100644
--- a/apps/web/app/(app)/layout.tsx
+++ b/apps/web/app/(app)/layout.tsx
@@ -1,12 +1,13 @@
"use client"
+import { EnsureWorkspace } from "@/components/ensure-workspace"
import { MobileBanner } from "@/components/mobile-banner"
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<>
- {children}
+ {children}
>
)
}
diff --git a/apps/web/app/(app)/onboarding/welcome/page.tsx b/apps/web/app/(app)/onboarding/welcome/page.tsx
index a43c65d6..6101b070 100644
--- a/apps/web/app/(app)/onboarding/welcome/page.tsx
+++ b/apps/web/app/(app)/onboarding/welcome/page.tsx
@@ -19,7 +19,30 @@ import {
} from "./layout"
import { gapVariants, orbVariants } from "@/lib/variants"
import { authClient } from "@lib/auth"
+import { useAuth } from "@lib/auth-context"
import { analytics } from "@/lib/analytics"
+import { toast } from "sonner"
+
+function generateSlugFromName(value: string) {
+ return (
+ value
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/(^-|-$)/g, "") || "org"
+ )
+}
+
+function generateOrgSlug(name: string) {
+ const base = generateSlugFromName(name.trim())
+ const randomNum = Math.floor(100000 + Math.random() * 900000)
+ return `${base}-${randomNum}`
+}
+
+function generateUsername(name: string) {
+ const base = generateSlugFromName(name.trim())
+ const randomNum = Math.floor(100000 + Math.random() * 900000)
+ return `${base}${randomNum}`
+}
function UserSupermemory({ name }: { name: string }) {
return (
@@ -78,20 +101,67 @@ export default function WelcomePage() {
goToStep,
} = useWelcomeContext()
+ const { refetchOrganizations, setActiveOrg } = useAuth()
+
const handleSubmit = async () => {
localStorage.setItem("username", name)
if (name.trim()) {
setIsSubmitting(true)
try {
- await authClient.updateUser({ displayUsername: name.trim() })
- } catch (error) {
- console.error("Failed to update displayUsername:", error)
- }
+ await authClient.updateUser({
+ displayUsername: name.trim(),
+ username: generateUsername(name.trim()),
+ })
- analytics.onboardingNameSubmitted({ name_length: name.trim().length })
- goToStep("greeting")
- setIsSubmitting(false)
+ const refetchResult = await refetchOrganizations()
+ const refetchData = (
+ refetchResult as { data?: unknown[] | null | undefined }
+ )?.data
+ const existingOrgs = Array.isArray(refetchData) ? refetchData : []
+
+ if (existingOrgs.length > 0) {
+ analytics.onboardingNameSubmitted({
+ name_length: name.trim().length,
+ })
+ goToStep("greeting")
+ return
+ }
+
+ const uniqueSlug = generateOrgSlug(name.trim())
+ const completedAt = new Date().toISOString()
+ const newOrg = await authClient.organization.create({
+ name: name.trim(),
+ slug: uniqueSlug,
+ metadata: {
+ signupSource: "consumer",
+ webOnboarding: {
+ completedAt: null,
+ steps: {
+ welcomeInput: {
+ startedAt: completedAt,
+ completedAt,
+ data: {},
+ },
+ },
+ },
+ },
+ })
+
+ await setActiveOrg(newOrg.slug)
+
+ analytics.onboardingNameSubmitted({ name_length: name.trim().length })
+ goToStep("greeting")
+ } catch (error) {
+ console.error("Onboarding submit failed:", error)
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Could not set up your workspace. Please try again.",
+ )
+ } finally {
+ setIsSubmitting(false)
+ }
}
}
diff --git a/apps/web/components/ensure-workspace.tsx b/apps/web/components/ensure-workspace.tsx
new file mode 100644
index 00000000..a9bd8823
--- /dev/null
+++ b/apps/web/components/ensure-workspace.tsx
@@ -0,0 +1,21 @@
+"use client"
+
+import { useEffect } from "react"
+import { usePathname, useRouter } from "next/navigation"
+import { useAuth } from "@lib/auth-context"
+
+export function EnsureWorkspace({ children }: { children: React.ReactNode }) {
+ const pathname = usePathname()
+ const router = useRouter()
+ const { session, organizations, isRestoring } = useAuth()
+
+ useEffect(() => {
+ if (!session) return
+ if (isRestoring || organizations === null) return
+ if (organizations.length > 0) return
+ if (pathname.startsWith("/onboarding")) return
+ router.replace("/onboarding/welcome?step=input")
+ }, [session, organizations, isRestoring, pathname, router])
+
+ return children
+}
diff --git a/packages/lib/auth-context.tsx b/packages/lib/auth-context.tsx
index caf7b6d5..45ffc5b4 100644
--- a/packages/lib/auth-context.tsx
+++ b/packages/lib/auth-context.tsx
@@ -12,13 +12,21 @@ import { authClient, useSession } from "./auth"
type Organization = typeof authClient.$Infer.ActiveOrganization
type SessionData = NonNullable["data"]>
+type OrganizationListItem = NonNullable<
+ ReturnType["data"]
+>[number]
+
+const STORAGE_KEY = "supermemory-consumer-last-org-slug"
interface AuthContextType {
session: SessionData["session"] | null
user: SessionData["user"] | null
org: Organization | null
+ organizations: OrganizationListItem[] | null
+ isRestoring: boolean
setActiveOrg: (orgSlug: string) => Promise
updateOrgMetadata: (partial: Record) => void
+ refetchOrganizations: () => Promise
}
const AuthContext = createContext(undefined)
@@ -26,17 +34,30 @@ const AuthContext = createContext(undefined)
export function AuthProvider({ children }: { children: ReactNode }) {
const { data: session } = useSession()
const [org, setOrg] = useState(null)
- const { data: orgs } = authClient.useListOrganizations()
+ const [isRestoring, setIsRestoring] = useState(true)
+ const {
+ data: orgsData,
+ refetch: refetchOrgsQuery,
+ isPending: orgsPending,
+ } = authClient.useListOrganizations()
- const setActiveOrg = async (slug: string) => {
+ const organizations: OrganizationListItem[] | null =
+ session?.session == null ? null : orgsPending ? null : (orgsData ?? [])
+
+ const refetchOrganizations = useCallback(
+ () => Promise.resolve(refetchOrgsQuery()),
+ [refetchOrgsQuery],
+ )
+
+ const setActiveOrg = useCallback(async (slug: string) => {
if (!slug) return
const activeOrg = await authClient.organization.setActive({
organizationSlug: slug,
})
setOrg(activeOrg)
- localStorage.setItem("supermemory-consumer-last-org-slug", slug)
- }
+ localStorage.setItem(STORAGE_KEY, slug)
+ }, [])
const updateOrgMetadata = useCallback((partial: Record) => {
setOrg((prev) => {
@@ -51,23 +72,81 @@ export function AuthProvider({ children }: { children: ReactNode }) {
})
}, [])
- // biome-ignore lint/correctness/useExhaustiveDependencies: ignoring the setActiveOrg dependency
useEffect(() => {
- if (!session?.session.activeOrganizationId || !orgs) return
-
- const savedSlug = localStorage.getItem("supermemory-consumer-last-org-slug")
-
- if (savedSlug && orgs.find((o) => o.slug === savedSlug)) {
- setActiveOrg(savedSlug)
+ if (!session?.session) {
+ setIsRestoring(false)
+ setOrg(null)
return
}
- if (savedSlug) localStorage.removeItem("supermemory-consumer-last-org-slug")
- authClient.organization.getFullOrganization().then(setOrg)
- }, [session?.session.activeOrganizationId, orgs])
+ if (orgsPending || orgsData === undefined) {
+ setIsRestoring(true)
+ return
+ }
+
+ const orgs = orgsData ?? []
+ let cancelled = false
+
+ const run = async () => {
+ try {
+ if (orgs.length === 0) {
+ if (!cancelled) setOrg(null)
+ return
+ }
+
+ const activeOrgId = session.session.activeOrganizationId
+
+ if (orgs.length === 1) {
+ const one = orgs[0]
+ if (!one) return
+ if (activeOrgId === one.id) {
+ const full = await authClient.organization.getFullOrganization()
+ if (!cancelled) setOrg(full)
+ } else {
+ await setActiveOrg(one.slug)
+ }
+ return
+ }
+
+ const savedSlug = localStorage.getItem(STORAGE_KEY)
+ if (savedSlug) {
+ const match = orgs.find((o) => o.slug === savedSlug)
+ if (match) {
+ if (activeOrgId === match.id) {
+ const full = await authClient.organization.getFullOrganization()
+ if (!cancelled) setOrg(full)
+ } else {
+ await setActiveOrg(savedSlug)
+ }
+ return
+ }
+ localStorage.removeItem(STORAGE_KEY)
+ }
+
+ if (activeOrgId) {
+ const fromList = orgs.find((o) => o.id === activeOrgId)
+ if (fromList) {
+ const full = await authClient.organization.getFullOrganization()
+ if (!cancelled) setOrg(full)
+ return
+ }
+ }
+
+ const full = await authClient.organization.getFullOrganization()
+ if (!cancelled) setOrg(full)
+ } catch (error) {
+ console.error("Failed to restore organization:", error)
+ } finally {
+ if (!cancelled) setIsRestoring(false)
+ }
+ }
+
+ void run()
+ return () => {
+ cancelled = true
+ }
+ }, [session, orgsData, orgsPending, setActiveOrg])
- // When a session exists and there is a pending login method recorded,
- // promote it to the last-used method (successful login) and clear pending.
useEffect(() => {
if (typeof window === "undefined") return
if (!session?.session) return
@@ -83,14 +162,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
if (pendingMethod) {
const now = Date.now()
const ts = pendingTsRaw ? Number.parseInt(pendingTsRaw, 10) : Number.NaN
- const isFresh = Number.isFinite(ts) && now - ts < 10 * 60 * 1000 // 10 minutes TTL
+ const isFresh = Number.isFinite(ts) && now - ts < 10 * 60 * 1000
if (isFresh) {
localStorage.setItem("supermemory-last-login-method", pendingMethod)
}
}
} catch {}
- // Always clear pending markers once a session is present
try {
localStorage.removeItem("supermemory-pending-login-method")
localStorage.removeItem("supermemory-pending-login-timestamp")
@@ -101,10 +179,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
{children}