diff --git a/apps/dashboard/app/(authenticated)/layout.tsx b/apps/dashboard/app/(authenticated)/layout.tsx
index a2aa041..563cb51 100644
--- a/apps/dashboard/app/(authenticated)/layout.tsx
+++ b/apps/dashboard/app/(authenticated)/layout.tsx
@@ -6,6 +6,7 @@ import { DashboardLayout } from "@/base/components/layout/dashboard"
import { useAuth } from "@/shared/hooks/use-auth"
import { navGroups } from "@/config/navGroups"
import { getAuthCookies } from "@/modules/auth/auth.actions"
+import { OnboardingGate } from "@/shared/components/onboarding-gate"
import { redirect } from "next/navigation"
@@ -38,7 +39,7 @@ export default async function AuthenticatedLayout({
return (
} user={userInfo}>
- {children}
+ {children}
)
}
diff --git a/apps/dashboard/modules/settings/company/settings-form.tsx b/apps/dashboard/modules/settings/company/settings-form.tsx
index 9946e60..a3b0371 100644
--- a/apps/dashboard/modules/settings/company/settings-form.tsx
+++ b/apps/dashboard/modules/settings/company/settings-form.tsx
@@ -4,7 +4,7 @@ import { AlertTriangle, Save } from "lucide-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
-import { useQuery } from "@tanstack/react-query"
+import { useQuery, useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner"
import { Button } from "@/shared/components/ui/button"
@@ -122,6 +122,7 @@ function mapFormToPayload(values: SettingsFormValues) {
export function SettingsForm() {
const api = useAuthApi()
+ const queryClient = useQueryClient()
const form = useForm({
resolver: zodResolver(settingsFormSchema) as any,
@@ -154,6 +155,9 @@ export function SettingsForm() {
mutationFn: (values: SettingsFormValues) => api.settings.update(mapFormToPayload(values)),
onSuccess: () => {
toast.success("Settings saved successfully")
+ // Refresh the settings query so the onboarding gate releases once
+ // the company info has been filled in.
+ queryClient.invalidateQueries({ queryKey: [SETTINGS_ROUTES.INDEX] })
},
onError: () => {
toast.error("Failed to save settings")
diff --git a/apps/dashboard/shared/components/onboarding-gate.tsx b/apps/dashboard/shared/components/onboarding-gate.tsx
new file mode 100644
index 0000000..99a920a
--- /dev/null
+++ b/apps/dashboard/shared/components/onboarding-gate.tsx
@@ -0,0 +1,72 @@
+"use client"
+
+import { useEffect, useMemo } from "react"
+import { usePathname, useRouter } from "next/navigation"
+import { useQuery } from "@tanstack/react-query"
+import { Loader2 } from "lucide-react"
+
+import { useAuthApi } from "@/shared/useApi"
+import { SETTINGS_ROUTES } from "@garage/api"
+
+const COMPANY_SETUP_PATH = "/settings/company"
+
+function FullScreenLoader({ label }: { label: string }) {
+ return (
+
+ )
+}
+
+/**
+ * Onboarding gate.
+ *
+ * After login/activation the workspace's company information must be filled in
+ * before the rest of the app is usable. While the settings record loads we show
+ * a full-screen loader; if the company isn't set up yet we force the user onto
+ * the company-settings page and block navigation to any other route until it is.
+ */
+export function OnboardingGate({ children }: { children: React.ReactNode }) {
+ const api = useAuthApi()
+ const router = useRouter()
+ const pathname = usePathname()
+
+ const { data, isLoading, isError } = useQuery({
+ queryKey: [SETTINGS_ROUTES.INDEX],
+ queryFn: () => api.settings.fetch(),
+ staleTime: 5 * 60 * 1000,
+ retry: 1,
+ })
+
+ const isCompanyFilled = useMemo(() => {
+ const raw = (data as any)?.data
+ const record = Array.isArray(raw) ? raw[0] : raw
+ return Boolean(record?.id) && Boolean(String(record?.name ?? "").trim())
+ }, [data])
+
+ const onCompanyPage = pathname?.startsWith(COMPANY_SETUP_PATH) ?? false
+
+ // Onboarding is required once we know (loaded, no error) the company isn't set up.
+ // Fail open on error so a transient API failure can never lock the user out.
+ const mustOnboard = !isLoading && !isError && !isCompanyFilled
+
+ useEffect(() => {
+ if (mustOnboard && !onCompanyPage) {
+ router.replace(COMPANY_SETUP_PATH)
+ }
+ }, [mustOnboard, onCompanyPage, router])
+
+ // Initial load — determine onboarding status before showing anything.
+ if (isLoading) {
+ return
+ }
+
+ // Needs onboarding and not yet on the company page → show loader while redirecting,
+ // so other pages never flash or become usable.
+ if (mustOnboard && !onCompanyPage) {
+ return
+ }
+
+ return <>{children}>
+}