Skip to main content
doc0blade47/mem0documentation home
Sign in

Dashboard Architecture

Last updated Oct 11, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

The Mem0 self-hosted dashboard is built as a Next.js 15 single-page application that provides a comprehensive web interface for managing memories, system configuration, API keys, and administrative operations. Powered by React 19, Redux Toolkit, and Tailwind CSS, the dashboard architecture implements a modular client layout alongside a secure Next.js middleware routing layer. It orchestrates state management, automated Axios token interception, and first-run initialization flows to seamlessly connect administrators with the underlying FastAPI backend.

Sources: server/dashboard/package.json:1-84, server/dashboard/src/middleware.ts:1-63

Application Layout and Provider Hierarchy

Root Layout Structure and Font Integration

The dashboard architecture organizes layouts across distinct Next.js route groups including root, authentication, and setup. The root client layout defined in DashboardClientLayout injects typography variables and provider trees into the DOM. It loads five font configurations (Inter, InterDisplay, Roboto, Fustat, and DMMono) from local font utilities and applies their CSS variable declarations alongside base font styles directly onto the document body.

export function DashboardClientLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body
        className={cn(
          Inter.className,
          InterDisplay.variable,
          Roboto.variable,
          Fustat.variable,
          DMMono.variable,
        )}
        suppressHydrationWarning
      >

Sources: server/dashboard/src/app/root/dashboard-client-layout.tsx:22-38

Similarly, the setup and authentication route layouts establish parallel HTML document roots that load the identical font configuration sets and enforce suppress hydration warnings to prevent server-client attribute mismatches.

return (
  <html
    lang="en"
    className={cn(
      Fustat.variable,
      InterDisplay.variable,
      Inter.variable,
      Roboto.variable,
      DMMono.variable,
    )}
    suppressHydrationWarning
  >
    <body className="font-fustat" suppressHydrationWarning>

Sources: server/dashboard/src/app/setup/layout.tsx:17-29, server/dashboard/src/app/auth/layout.tsx:18-30

Theme Injection and Provider Composition

Provider hierarchy wraps application route trees in a specific nested order to ensure state stores, authentication contexts, theme properties, and tooltips are available to all child components. In the root client layout, the Redux store provider sits at the outermost boundary, followed in sequence by the authentication provider, next-themes provider, and the custom client layout.

<Provider store={store}>
  <AuthProvider>
    <ThemeProvider
      attribute="class"
      defaultTheme="light"
      enableSystem
      disableTransitionOnChange
    >
      <ClientLayout>{children}</ClientLayout>
      <Toaster />
    </ThemeProvider>
  </AuthProvider>
</Provider>

Sources: server/dashboard/src/app/root/dashboard-client-layout.tsx:39-51

Theme injection is handled via ThemeProvider, a wrapper around next-themes that accepts attributes, default settings, and transition rules. Across route layouts, theme configuration values differ based on authentication or installation state requirements:

Route Group / LayoutDefault ThemeEnable SystemDisable Transition On ChangeAttribute
Root Dashboard (dashboard-client-layout.tsx)lighttruetrueclass
Setup Wizard (setup/layout.tsx)lightfalsetrueclass
Authentication (auth/layout.tsx)systemtruetrueclass

Sources: server/dashboard/src/app/root/dashboard-client-layout.tsx:41-46, server/dashboard/src/app/setup/layout.tsx:31-35, server/dashboard/src/app/auth/layout.tsx:32-37

Note

The setup wizard layout intentionally disables system theme detection (enableSystem={false}), forcing a default light theme during initial installation, whereas authentication and dashboard layouts support dynamic system preference matching.

Sources: server/dashboard/src/app/setup/layout.tsx:31-35, server/dashboard/src/app/auth/layout.tsx:32-37

Client Layout and Sidebar Geometry

The inner ClientLayout component enforces session validation logic. It evaluates the current pathname against public prefixes (/login and /setup) and monitors authentication state. If a user is unauthenticated on a protected path, it renders an AuthLoadingState containing a theme-aware logo and linear progress indicator while executing a router replacement to /login.

const isPublicPage =
  pathname.startsWith("/login") || pathname.startsWith("/setup");
 
useEffect(() => {
  if (!isPublicPage && !isLoading && !user) {
    router.replace("/login");
  }
}, [isLoading, isPublicPage, router, user]);
 
if (isLoading && !isPublicPage) {
  return <AuthLoadingState />;
}
 
if (!isPublicPage && !user) {
  return <AuthLoadingState />;
}

Sources: server/dashboard/src/app/root/clientLayout.tsx:31-46

Within the dashboard sub-layout, structural dimensions for navigation and content containers are governed by fixed constants exported from the client layout module.

export const SIDEBAR_WIDTH = 180;
export const COLLAPSED_SIDEBAR_WIDTH = 64;
export const COLLAPSED_SIDEBAR_PADDING = 16;
export const COLLAPSED_SIDEBAR_WIDTH_WITHOUT_PADDING =
  COLLAPSED_SIDEBAR_WIDTH - COLLAPSED_SIDEBAR_PADDING;

Sources: server/dashboard/src/app/root/clientLayout.tsx:10-14

The dashboard view layout reads sidebar collapse state from the Redux store (state.layout.isSidebarCollapsed) to dynamically calculate horizontal offsets and content area width styles for the main scrollable viewport container.

const isSidebarCollapsed = useSelector(
  (state: RootState) => state.layout.isSidebarCollapsed,
);
 
return (
  <>
    <NavWrapper />
    <div
      className="mt-[48px] rounded-tl-lg relative h-[calc(100vh-56px)] bg-surface-default-primary border border-memBorder-primary overflow-hidden transition-all duration-300 ease-in-out font-fustat"
      style={{
        left: `${isSidebarCollapsed ? COLLAPSED_SIDEBAR_WIDTH : SIDEBAR_WIDTH}px`,
        width: `calc(100vw - ${isSidebarCollapsed ? COLLAPSED_SIDEBAR_WIDTH + 8 : SIDEBAR_WIDTH + 8}px)`,
      }}

Sources: server/dashboard/src/app/root/dashboard/layout.tsx:15-27

Client-Side Authentication Session Management

AuthProvider Context and Session Lifecycle

Client-side session handling centers around AuthProvider, a client component ("use client") that manages user profile state, loading indicators, and authentication actions. The AuthContext supplies consumer hooks with user records, role checking via isAdmin, and actions for login, register, logout, and refreshUser.

export interface AuthUser {
  id: string;
  name: string;
  email: string;
  role: string;
  created_at: string;
}
 
interface AuthContextValue {
  user: AuthUser | null;
  isLoading: boolean;
  isAdmin: boolean;
  login: (email: string, password: string) => Promise<void>;
  register: (name: string, email: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
  refreshUser: () => Promise<void>;
}
 
export const AuthContext = createContext<AuthContextValue>({
  user: null,
  isLoading: true,
  isAdmin: false,
  login: async () => {},
  register: async () => {},
  logout: async () => {},
  refreshUser: async () => {},
});

Sources: server/dashboard/src/lib/auth.tsx:13-39

When the component mounts, an effect hook invokes refreshSession() to exchange an HTTP-only refresh cookie for an access token. If successful, it calls loadUser() to fetch user details from AUTH_ENDPOINTS.ME. In case of failure or network rejection, user resets to null, and isLoading sets to false.

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<AuthUser | null>(null);
  const [isLoading, setIsLoading] = useState(true);
 
  const loadUser = useCallback(async () => {
    const res = await api.get<AuthUser>(AUTH_ENDPOINTS.ME);
    setUser(res.data);
  }, []);
 
  useEffect(() => {
    let active = true;
    (async () => {
      try {
        const ok = await refreshSession();
        if (ok && active) await loadUser();
      } catch {
        if (active) setUser(null);
      } finally {
        if (active) setIsLoading(false);
      }
    })();
    return () => {
      active = false;
    };
  }, [loadUser]);

Sources: server/dashboard/src/lib/auth.tsx:64-88

Warning

Component unmounting sets active = false to prevent asynchronous state updates on unmounted component instances during initial session validation.

Sources: server/dashboard/src/lib/auth.tsx:73-87

Client authentication helpers communicate with Next.js API route handlers to read, write, and purge session tokens. The backend refresh route manages secure cookies named mem0_refresh_token using parameters configured in COOKIE_OPTIONS.

const COOKIE_NAME = "mem0_refresh_token";
 
function shouldUseSecureCookie() {
  const dashboardUrl = process.env.DASHBOARD_URL;
  if (!dashboardUrl) {
    return process.env.NODE_ENV === "production";
  }
 
  try {
    return new URL(dashboardUrl).protocol === "https:";
  } catch {
    return process.env.NODE_ENV === "production";
  }
}
 
const COOKIE_OPTIONS = {
  httpOnly: true,
  secure: shouldUseSecureCookie(),
  sameSite: "lax" as const,
  path: "/",
  maxAge: 30 * 24 * 60 * 60, // 30 days
};

Sources: server/dashboard/src/app/api/auth/refresh/route.ts:6-27

Route MethodActionRequest PayloadResponseError Code
POSTValidates existing refresh token and issues new tokensNone (reads cookie){ access_token }401
PUTStores refresh token in HTTP-only cookie{ refresh_token }{ ok: true }400
DELETEClears refresh token cookieNone{ ok: true }—

Sources: server/dashboard/src/app/api/auth/refresh/route.ts:29-75

Authentication hooks like useAuth provide clean access to the context throughout component trees by wrapping useContext(AuthContext).

export function useAuth() {
  return useContext(AuthContext);
}

Sources: server/dashboard/src/hooks/use-auth.ts:6-8

Route Protection and Edge Middleware

Middleware Execution and Path Interception

The dashboard relies on Next.js edge middleware running prior to route resolution to inspect incoming requests and enforce authentication policies. The middleware matches requests against paths excluding static assets and static files as defined by the matcher configuration.

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|fonts|images|icons).*)"],
};

Sources: server/dashboard/src/middleware.ts:65-67

Incoming path evaluation begins by checking against a static array of public prefixes. If a request pathname starts with any entry in PUBLIC_PATHS, the middleware bypasses all checks and allows execution to proceed.

const PUBLIC_PATHS = [
  "/_next",
  "/api/auth",
  "/api/health",
  "/fonts",
  "/favicon",
];
 
export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
 
  if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) {
    return NextResponse.next();
  }

Sources: server/dashboard/src/middleware.ts:5-18

Public Path PrefixPurpose
/_nextNext.js internal static chunks and assets
/api/authAuthentication endpoints and token refresh routes
/api/healthSystem health check inspection route
/fontsApplication font files
/faviconBrowser icon assets

Sources: server/dashboard/src/middleware.ts:5-11

Setup Status Verification and Routing

When a request targets root paths (/, /login, or /setup), the middleware queries the backend server setup status endpoint to determine whether initial deployment registration is required.

if (pathname === "/" || pathname === "/login" || pathname === "/setup") {
  try {
    const res = await fetch(
      `${getServerApiUrl()}${AUTH_ENDPOINTS.SETUP_STATUS}`,
    );

Sources: server/dashboard/src/middleware.ts:22-26

Warning

If the backend API is unreachable during setup status checks, the network request throws an error which is caught locally, causing the middleware to gracefully fall through to standard navigation rules without blocking requests.

Sources: server/dashboard/src/middleware.ts:37-39

Depending on the boolean flag needsSetup returned from the API, the middleware enforces routing rules:

if (res.ok) {
  const { needsSetup } = await res.json();
 
  if (needsSetup && pathname !== "/setup") {
    return NextResponse.redirect(new URL("/setup", request.url));
  }
  if (!needsSetup && pathname === "/setup") {
    return NextResponse.redirect(new URL("/login", request.url));
  }
}

Sources: server/dashboard/src/middleware.ts:27-36

Token Verification and Protected Navigation

For routes outside the public or setup pathways, the middleware checks for the presence of the HTTP-only refresh token cookie (mem0_refresh_token).

const hasRefreshToken = request.cookies.has("mem0_refresh_token");

Sources: server/dashboard/src/middleware.ts:20-20

If a user visits the application root (/), redirection behavior branches based on whether the refresh token cookie exists. If present, the user is forwarded to /dashboard/requests; otherwise, they are routed to /login. Similarly, base dashboard paths (/dashboard or /dashboard/) automatically rewrite to /dashboard/requests.

if (pathname === "/") {
  return NextResponse.redirect(
    new URL(hasRefreshToken ? "/dashboard/requests" : "/login", request.url),
  );
}
 
if (pathname === "/dashboard" || pathname === "/dashboard/") {
  return NextResponse.redirect(new URL("/dashboard/requests", request.url));
}

Sources: server/dashboard/src/middleware.ts:46-54

When a protected route is accessed without the mem0_refresh_token cookie, the middleware constructs a redirect response targeting /login and appends the original destination to a next query parameter so the client can resume its path after authentication.

if (!hasRefreshToken) {
  const loginUrl = new URL("/login", request.url);
  loginUrl.searchParams.set("next", pathname);
  return NextResponse.redirect(loginUrl);
}

Sources: server/dashboard/src/middleware.ts:56-60

Axios Client and Token Interception

Centralized API Client Setup and Bearer Token Injection

The dashboard instantiates a centralized Axios instance configured with a base URL sourced from process.env.NEXT_PUBLIC_API_URL. To authenticate outgoing requests, a request interceptor inspects the in-memory token cache and injects a Bearer authorization header if a token exists.

let cachedToken: string | null = null;
const LOGIN_PATH = "/login";
 
export const setAccessToken = (token: string | null) => {
  cachedToken = token;
};
 
export const getAccessToken = (): string | null => {
  return cachedToken;
};
 

Sources: server/dashboard/src/utils/api.ts:3-13

const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
});
 
api.interceptors.request.use(
  async (config) => {
    if (cachedToken) {
      config.headers = config.headers ?? {};
      config.headers.Authorization = `Bearer ${cachedToken}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  },
);

Sources: server/dashboard/src/utils/api.ts:42-57

Note

Tokens are held exclusively in runtime memory via cachedToken and manipulated using setAccessToken and getAccessToken helpers, preventing persistent storage vulnerabilities on the client side.

Sources: server/dashboard/src/utils/api.ts:3-12

Response Interception and Token Refresh Flow

When an API request encounters a 401 Unauthorized response, the response interceptor triggers an automated token recovery sequence. It first clears the stored token via handleTokenError and issues a POST request to /api/auth/refresh with credentials included.

const handleTokenError = () => {
  cachedToken = null;
};

Sources: server/dashboard/src/utils/api.ts:14-16

const refreshAccessToken = async () => {
  const refreshResponse = await fetch("/api/auth/refresh", {
    method: "POST",
    credentials: "include",
  });
 
  if (!refreshResponse.ok) {
    return null;
  }
 
  const data = await refreshResponse.json();
  setAccessToken(data.access_token);
  return data.access_token as string;
};

Sources: server/dashboard/src/utils/api.ts:24-37

If the refresh endpoint returns a valid new access token, the client updates its cache, attaches the new Bearer token to the original request configuration, and retries the failed call. If the refresh attempt fails or throws an exception, the client clears the token cache and executes a window redirect to /login.

api.interceptors.response.use(
  (response) => response,
  async (error: AxiosError<{ error?: string }>) => {
    if (error.response?.status === 401) {
      handleTokenError();
 
      try {
        const nextToken = await refreshAccessToken();
        if (nextToken && error.config) {
          error.config.headers = error.config.headers ?? {};
          error.config.headers.Authorization = `Bearer ${nextToken}`;
          return api.request(error.config);
        }
      } catch {}
 
      handleTokenError();
      redirectToLogin();
    }

Sources: server/dashboard/src/utils/api.ts:59-76

const redirectToLogin = () => {
  if (typeof window !== "undefined") {
    window.location.href = LOGIN_PATH;
  }
};

Sources: server/dashboard/src/utils/api.ts:18-22

Streaming Request Handling

Alongside the standard Axios instance, the API module exposes a custom postStream helper designed to handle server-sent event or streaming payload requests via the native fetch API.

const postStream = async (url: string, data: unknown): Promise<Response> => {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}${url}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: cachedToken ? `Bearer ${cachedToken}` : "",
    },
    body: JSON.stringify(data),
  });

Sources: server/dashboard/src/utils/api.ts:86-94

The postStream function evaluates response statuses explicitly, throwing an unauthorized error and triggering redirectToLogin if a 401 status code occurs, or parsing error payloads when requests fail.

  if (response.status === 401) {
    handleTokenError();
    redirectToLogin();
    throw new Error("Unauthorized");
  }
 
  if (!response.ok) {
    const errorData = await response.json().catch(() => ({}));
    throw new Error(errorData.error || "Request failed");
  }
 
  return response;
};

Sources: server/dashboard/src/utils/api.ts:96-108

Data Fetching Hook Lifecycle

The useApiQuery Execution Model

The useApiQuery custom hook manages asynchronous data fetching, loading flags, error strings, and manual refetch capabilities for dashboard components like ApiKeysPage and ConfigurationPage. It accepts a generic fetcher function alongside a UseApiQueryOptions configuration object specifying enabled, errorToast, and initialData.

interface UseApiQueryOptions<T> {
  enabled?: boolean;
  errorToast?: string;
  initialData?: T;
}
 
interface UseApiQueryResult<T> {
  data: T | undefined;
  isLoading: boolean;
  error: string;
  refetch: () => Promise<void>;
}
 
export function useApiQuery<T>(
  fetcher: () => Promise<T>,
  options: UseApiQueryOptions<T> = {},
): UseApiQueryResult<T> {

Sources: server/dashboard/src/hooks/use-api-query.ts:5-21

State management tracks resolved items via data, active request status via isLoading, and failure messages via error. A React useRef tracks the latest fetcher reference so callbacks execute the most up-to-date query logic without capturing stale closures.

const [data, setData] = useState<T | undefined>(initialData);
const [isLoading, setIsLoading] = useState(enabled);
const [error, setError] = useState("");
 
const fetcherRef = useRef(fetcher);
fetcherRef.current = fetcher;
 

Sources: server/dashboard/src/hooks/use-api-query.ts:24-30

Call-Chain Execution Walkthrough

  1. ApiKeysPage or ConfigurationPage invokes useApiQuery during component initialization, passing a custom async fetcher and options.
  2. useApiQuery defines the run callback via useCallback, which marks isLoading as true and clears any existing error string.
  3. run executes fetcherRef.current() to retrieve network data, storing the resolved value in data upon success.
  4. If an exception occurs, getErrorMessage processes the error alongside any provided errorToast label, populates error, and invokes toast() if an error toast option was specified.
  5. The finally block sets isLoading to false, and a useEffect hook triggers run automatically on mount if enabled is true.
const run = useCallback(async () => {
  setIsLoading(true);
  setError("");
  try {
    setData(await fetcherRef.current());
  } catch (err) {
    const message = getErrorMessage(err, errorToast || "Request failed");
    setError(message);
    if (errorToast) {
      toast({
        title: errorToast,
        description: message,
        variant: "destructive",
      });
    }
  } finally {
    setIsLoading(false);
  }
}, [errorToast]);
 
useEffect(() => {
  if (enabled) void run();
}, [enabled, run]);

Sources: server/dashboard/src/hooks/use-api-query.ts:31-53

Sources: server/dashboard/src/hooks/use-api-query.ts:31-55, server/dashboard/src/app/root/dashboard/api-keys/page.tsx:41-47, server/dashboard/src/app/root/dashboard/configuration/page.tsx:41-47

Options and Return Signature Reference

Property / ParameterTypeDefaultDescription
enabledbooleantrueControls whether the query executes automatically on initial component mount.
errorToaststringundefinedTitle string passed to the centralized notification toast if the request fails.
initialDataTundefinedFallback data value assigned before the initial fetch completes.
dataT | undefinedinitialDataThe successfully resolved payload returned by the fetcher function.
isLoadingbooleanenabledBoolean flag tracking whether an asynchronous fetch operation is currently in flight.
errorstring""Formatted error message string populated when a fetch exception occurs.
refetch() => Promise<void>—Manual trigger function to re-run the asynchronous fetcher on demand.
interface UseApiQueryOptions<T> {
  enabled?: boolean;
  errorToast?: string;
  initialData?: T;
}
 
interface UseApiQueryResult<T> {
  data: T | undefined;
  isLoading: boolean;
  error: string;
  refetch: () => Promise<void>;
}
 
export function useApiQuery<T>(
  fetcher: () => Promise<T>,
  options: UseApiQueryOptions<T> = {},
): UseApiQueryResult<T> {

Sources: server/dashboard/src/hooks/use-api-query.ts:5-21

return { data, isLoading, error, refetch: run };

Sources: server/dashboard/src/hooks/use-api-query.ts:55-55

Note

If errorToast is omitted from UseApiQueryOptions, failure messages update the local error state variable without dispatching an intrusive toast notification to the user interface.

Sources: server/dashboard/src/hooks/use-api-query.ts:37-45

Redux Toolkit State and Navigation

Redux Store Configuration and Root Reducer

The application state container is established using Redux Toolkit's configureStore helper, importing a consolidated rootReducer that merges all feature slices. Currently, the store manages a single slice under the layout key, and exports standard TypeScript definitions for RootState and AppDispatch.

import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./rootReducer";
 
const store = configureStore({
  reducer: rootReducer,
});
 
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;

Sources: server/dashboard/src/store/store.ts:1-10

import { combineReducers } from "redux";
import { layoutReducer } from "./reducers/layoutReducer";
 
const rootReducer = combineReducers({
  layout: layoutReducer,
});
 
export default rootReducer;

Sources: server/dashboard/src/store/rootReducer.ts:1-8

Export NameReturn Type / DefinitionPurpose
storeReturn type of configureStoreThe central Redux store instance wrapping rootReducer.
RootStateReturnType<typeof store.getState>Global state type used by selectors across components.
AppDispatchtypeof store.dispatchDispatch type for triggering slice actions.

Sources: server/dashboard/src/store/store.ts:8-10

Note

rootReducer uses Redux's combineReducers function, making it easy to append new feature slices alongside layout as the dashboard architecture expands.

Sources: server/dashboard/src/store/rootReducer.ts:1-6

Collapsible Sidebar Navigation and Layout State

The sidebar navigation layout relies on the isSidebarCollapsed boolean flag retrieved from state.layout.isSidebarCollapsed via Redux's useSelector. Depending on this state, components like MainNav and NavWrapper dynamically adapt their width, hide or display text labels, and toggle collapsible trigger groups.

const pathname = usePathname();
const isSidebarCollapsed = useSelector(
  (state: RootState) => state.layout.isSidebarCollapsed,
);
const [isCloudOpen, setIsCloudOpen] = React.useState(true);
 

Sources: server/dashboard/src/app/root/dashboard/components/main-nav.tsx:43-48

The navigation structure is divided into three primary sections: Activity, Cloud Features (wrapped in a local Collapsible component controlled by isCloudOpen), and Account. When the sidebar is collapsed, group labels are hidden, and menu items center their icons with tooltip text enabled.

<div className="flex flex-col gap-0">
  {!isSidebarCollapsed && (
    <SidebarGroupLabel className="mb-0">
      ACTIVITY
    </SidebarGroupLabel>
  )}
  {[
    {
      title: "Requests",
      url: "/dashboard/requests",
      icon: Activity,
      active: pathname === "/dashboard/requests",
    },
    {
      title: "Memories",
      url: "/dashboard/memories",
      icon: GalleryVerticalEnd,
      active: pathname === "/dashboard/memories",
    },
    {
      title: "Entities",
      url: "/dashboard/entities",
      icon: Users,
      active: pathname === "/dashboard/entities",
    },
  ].map((item) => (
    <SidebarMenuItem key={item.title}>
      <SidebarMenuButton
        asChild
        collapsed={isSidebarCollapsed}
        active={item.active}
        tooltip={isSidebarCollapsed ? item.title : undefined}
      >
        <Link
          href={item.url}
          className={cn(
            "flex items-center w-full",
            isSidebarCollapsed
              ? "justify-center mx-auto"
              : "gap-1.5",
          )}
        >
          <item.icon className="size-4 shrink-0" />
          {!isSidebarCollapsed && <span>{item.title}</span>}
        </Link>
      </SidebarMenuButton>
    </SidebarMenuItem>
  ))}
</div>
 
{isSidebarCollapsed && (
  <div className="h-[1px] w-full bg-memBorder-primary my-2" />
)}
 
<Collapsible
  open={isCloudOpen}
  onOpenChange={setIsCloudOpen}
  className="flex flex-col gap-0"
>
  {!isSidebarCollapsed && (
    <CollapsibleTrigger asChild>
      <SidebarGroupLabel className="cursor-pointer mb-0">
        CLOUD FEATURES
        <ChevronDown
          className={cn(
            "size-3 transition-transform duration-200",
            isCloudOpen ? "" : "-rotate-90",
          )}
        />
      </SidebarGroupLabel>
    </CollapsibleTrigger>
  )}
  <CollapsibleContent className="flex flex-col gap-0">
    {[
      {
        title: "Categories",
        url: "/dashboard/categories",
        icon: Tags,
      },
      {

Sources: server/dashboard/src/app/root/dashboard/components/main-nav.tsx:59-242

export default function NavWrapper() {
  const dispatch = useDispatch();
  const isSidebarCollapsed = useSelector(
    (state: RootState) => state.layout.isSidebarCollapsed,
  );
  const { user, logout } = useAuth();
 
  const instanceName = process.env.NEXT_PUBLIC_INSTANCE_NAME || "Mem0";
 
  const handleToggle = useCallback(() => {
    dispatch(toggleSidebar());
  }, [dispatch]);
 

Sources: server/dashboard/src/app/root/dashboard/components/nav-wrapper.tsx:31-43

Warning

Toggling the sidebar state updates both the fixed container width and the adjacent content offset calculations using constants like COLLAPSED_SIDEBAR_WIDTH and SIDEBAR_WIDTH.

Sources: server/dashboard/src/app/root/dashboard/components/nav-wrapper.tsx:46-51, server/dashboard/src/app/root/dashboard/components/nav-wrapper.tsx:136-141

First-Run Setup Wizard Flow

Multi-Step Wizard and Account Registration

The first-run setup wizard is orchestrated by SetupPage, which guides uninitialized server instances through a sequential five-step flow. The wizard tracks the active step index via local state and references an array of step titles defining the initialization lifecycle.

const STEP_TITLES = [
  "Create your admin account",
  "Review provider configuration",
  "Your API key",
  "Tell us your use case",
  "Test your setup",
];

Sources: server/dashboard/src/app/setup/page.tsx:45-51

Step IndexTitlePrimary Action / Handler
0Create your admin accounthandleStep1 validates email format, confirms password matching, enforces a minimum length of 8 characters, and invokes register(name, email, password).
1Review provider configurationhandleStep2 checks if configuration fields are dirty, builds provider payloads, and posts updates to MEMORY_ENDPOINTS.CONFIGURE.
2Your API keyhandleStep3 requests a new API key from API_KEY_ENDPOINTS.BASE using the specified key label.
3Tell us your use caseCaptures user use case selection from preset options and custom instructions.
4Test your setupExecutes a validation test message against the configured backend.

Sources: server/dashboard/src/app/setup/page.tsx:45-51, server/dashboard/src/app/setup/page.tsx:157-186, server/dashboard/src/app/setup/page.tsx:188-235, server/dashboard/src/app/setup/page.tsx:237-246

Warning

Step 1 registration enforces strict client-side validation: email must satisfy isValidEmail, password must equal confirmPassword, and password.length must be at least 8 characters before submitting credentials to the authentication provider.

Sources: server/dashboard/src/app/setup/page.tsx:160-173

Provider Configuration and Credentials

When the wizard advances to step index 1, useEffect triggers an asynchronous configuration loader. This routine fetches the effective server configuration and bundled providers concurrently using MEMORY_ENDPOINTS.CONFIGURE and MEMORY_ENDPOINTS.CONFIGURE_PROVIDERS.

const loadConfig = async () => {
  try {
    const [configRes, providersRes] = await Promise.all([
      api.get(MEMORY_ENDPOINTS.CONFIGURE),
      api.get<BundledProviders>(MEMORY_ENDPOINTS.CONFIGURE_PROVIDERS),
    ]);

Sources: server/dashboard/src/app/setup/page.tsx:112-117

The configuration loader extracts provider identifiers and model names for both Language Model (llm) and embedder subsystems, storing both current state values and initial baselines to detect subsequent modifications.

const llmProv = config?.llm?.provider || "";
const llmMod = config?.llm?.config?.model || "";
const embProv = config?.embedder?.provider || "";
const embMod = config?.embedder?.config?.model || "";
 
setLlmProvider(llmProv);
setLlmModel(llmMod);
setEmbedderProvider(embProv);
setEmbedderModel(embMod);
setInitialLlmProvider(llmProv);
setInitialLlmModel(llmMod);
setInitialEmbedderProvider(embProv);
setInitialEmbedderModel(embMod);
setServerHasLlmKey(!!config?.llm?.config?.api_key);
setProviders(providersRes.data);

Sources: server/dashboard/src/app/setup/page.tsx:124-138

const USE_CASE_PRESETS = [
  "Personal assistant",
  "Coding agent",
  "Customer support",
  "Research",
  "Therapy / journaling",
];

Sources: server/dashboard/src/app/setup/page.tsx:55-61

Tip

If no changes are detected when submitting step 2 (dirty evaluates to false), the wizard bypasses the API save request and immediately advances to step 2.

Sources: server/dashboard/src/app/setup/page.tsx:192-202

Was this page helpful?