diff --git a/web/src/main.tsx b/web/src/main.tsx
index 038d4ee6c..c1f9b3e4b 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -6,6 +6,8 @@ import { RouterProvider } from "react-router-dom";
 import "./i18n";
 import "./index.css";
 import router from "./router";
+// Configure MobX before importing any stores
+import "./store/config";
 import { initialUserStore } from "./store/user";
 import { initialWorkspaceStore } from "./store/workspace";
 import { applyThemeEarly } from "./utils/theme";
diff --git a/web/src/store/README.md b/web/src/store/README.md
new file mode 100644
index 000000000..498740b72
--- /dev/null
+++ b/web/src/store/README.md
@@ -0,0 +1,277 @@
+# Store Architecture
+
+This directory contains the application's state management implementation using MobX.
+
+## Overview
+
+The store architecture follows a clear separation of concerns:
+
+- **Server State Stores**: Manage data fetched from the backend API
+- **Client State Stores**: Manage UI preferences and transient state
+
+## Store Files
+
+### Server State Stores (API Data)
+
+| Store | File | Purpose |
+|-------|------|---------|
+| `memoStore` | `memo.ts` | Memo CRUD operations, optimistic updates |
+| `userStore` | `user.ts` | User authentication, settings, stats |
+| `workspaceStore` | `workspace.ts` | Workspace profile and settings |
+| `attachmentStore` | `attachment.ts` | File attachment management |
+
+**Features:**
+- ✅ Request deduplication (prevents duplicate API calls)
+- ✅ Structured error handling with `StoreError`
+- ✅ Computed property memoization for performance
+- ✅ Optimistic updates (immediate UI feedback)
+- ✅ Automatic caching
+
+### Client State Stores (UI State)
+
+| Store | File | Purpose | Persistence |
+|-------|------|---------|-------------|
+| `viewStore` | `view.ts` | Display preferences (sort, layout) | localStorage |
+| `memoFilterStore` | `memoFilter.ts` | Active search filters | URL params |
+
+**Features:**
+- ✅ No API calls (instant updates)
+- ✅ localStorage persistence (viewStore)
+- ✅ URL synchronization (memoFilterStore - shareable links)
+
+### Utilities
+
+| File | Purpose |
+|------|---------|
+| `base-store.ts` | Base classes and factory functions |
+| `store-utils.ts` | Request deduplication, error handling, optimistic updates |
+| `config.ts` | MobX configuration |
+| `common.ts` | Shared constants and utilities |
+| `index.ts` | Centralized exports |
+
+## Usage Examples
+
+### Basic Store Usage
+
+```typescript
+import { memoStore, userStore, viewStore } from "@/store";
+import { observer } from "mobx-react-lite";
+
+const MyComponent = observer(() => {
+  // Access state
+  const memos = memoStore.state.memos;
+  const currentUser = userStore.state.currentUser;
+  const sortOrder = viewStore.state.orderByTimeAsc;
+
+  // Call actions
+  const handleCreate = async () => {
+    await memoStore.createMemo({ content: "Hello" });
+  };
+
+  const toggleSort = () => {
+    viewStore.toggleSortOrder();
+  };
+
+  return 
...
;
+});
+```
+
+### Server Store Pattern
+
+```typescript
+// Fetch data with automatic deduplication
+const memo = await memoStore.getOrFetchMemoByName("memos/123");
+
+// Update with optimistic UI updates
+await memoStore.updateMemo({ name: "memos/123", content: "Updated" }, ["content"]);
+
+// Errors are wrapped in StoreError
+try {
+  await memoStore.deleteMemo("memos/123");
+} catch (error) {
+  if (error instanceof StoreError) {
+    console.error(error.code, error.message);
+  }
+}
+```
+
+### Client Store Pattern
+
+```typescript
+// View preferences (persisted to localStorage)
+viewStore.setLayout("MASONRY");
+viewStore.toggleSortOrder();
+
+// Filters (synced to URL)
+memoFilterStore.addFilter({ factor: "tagSearch", value: "work" });
+memoFilterStore.removeFiltersByFactor("tagSearch");
+memoFilterStore.clearAllFilters();
+```
+
+## Creating New Stores
+
+### Server State Store
+
+```typescript
+import { StandardState, createServerStore } from "./base-store";
+import { createRequestKey, StoreError } from "./store-utils";
+
+class MyState extends StandardState {
+  dataMap: Record = {};
+
+  get items() {
+    return Object.values(this.dataMap);
+  }
+}
+
+const myStore = (() => {
+  const base = createServerStore(new MyState(), {
+    name: "myStore",
+    enableDeduplication: true,
+  });
+
+  const { state, executeRequest } = base;
+
+  const fetchItems = async () => {
+    return executeRequest(
+      createRequestKey("fetchItems"),
+      async () => {
+        const items = await api.fetchItems();
+        state.setPartial({ dataMap: items });
+        return items;
+      },
+      "FETCH_ITEMS_FAILED"
+    );
+  };
+
+  return { state, fetchItems };
+})();
+```
+
+### Client State Store
+
+```typescript
+import { StandardState } from "./base-store";
+
+class MyState extends StandardState {
+  preference: string = "default";
+
+  setPartial(partial: Partial) {
+    Object.assign(this, partial);
+    // Optional: persist to localStorage
+    localStorage.setItem("my-preference", JSON.stringify(this));
+  }
+}
+
+const myStore = (() => {
+  const state = new MyState();
+
+  const setPreference = (value: string) => {
+    state.setPartial({ preference: value });
+  };
+
+  return { state, setPreference };
+})();
+```
+
+## Best Practices
+
+### ✅ Do
+
+- Use `observer()` HOC for components that access store state
+- Call store actions from event handlers
+- Use computed properties for derived state
+- Handle errors from async store operations
+- Keep stores focused on a single domain
+
+### ❌ Don't
+
+- Don't mutate store state directly - use `setPartial()` or action methods
+- Don't call async store methods during render
+- Don't mix server and client state in the same store
+- Don't access stores outside of React components (except initialization)
+
+## Performance Tips
+
+1. **Computed Properties**: Use getters for derived state - they're memoized by MobX
+2. **Request Deduplication**: Automatic for server stores - prevents wasted API calls
+3. **Optimistic Updates**: Used in `updateMemo` - immediate UI feedback
+4. **Fine-grained Reactivity**: MobX only re-renders components that access changed properties
+
+## Testing
+
+```typescript
+import { memoStore } from "@/store";
+
+describe("memoStore", () => {
+  it("should fetch memos", async () => {
+    const memos = await memoStore.fetchMemos({ filter: "..." });
+    expect(memos).toBeDefined();
+  });
+
+  it("should cache memos", () => {
+    const memo = memoStore.getMemoByName("memos/123");
+    expect(memo).toBeDefined();
+  });
+});
+```
+
+## Migration Guide
+
+If you're migrating from old store patterns:
+
+1. **Replace direct state mutations** with `setPartial()`:
+   ```typescript
+   // Before
+   store.state.value = 5;
+
+   // After
+   store.state.setPartial({ value: 5 });
+   ```
+
+2. **Wrap API calls** with `executeRequest()`:
+   ```typescript
+   // Before
+   const data = await api.fetch();
+   state.data = data;
+
+   // After
+   return executeRequest("fetchData", async () => {
+     const data = await api.fetch();
+     state.setPartial({ data });
+     return data;
+   }, "FETCH_FAILED");
+   ```
+
+3. **Use StandardState** for new stores:
+   ```typescript
+   // Before
+   class State {
+     constructor() { makeAutoObservable(this); }
+   }
+
+   // After
+   class State extends StandardState {
+     // makeAutoObservable() called automatically
+   }
+   ```
+
+## Troubleshooting
+
+**Q: Component not re-rendering when state changes?**
+A: Make sure you wrapped it with `observer()` from `mobx-react-lite`.
+
+**Q: Getting "Cannot modify state outside of actions" error?**
+A: Use `state.setPartial()` instead of direct mutations.
+
+**Q: API calls firing multiple times?**
+A: Check that your store uses `createServerStore()` with deduplication enabled.
+
+**Q: localStorage not persisting?**
+A: Ensure your client store overrides `setPartial()` to call `localStorage.setItem()`.
+
+## Resources
+
+- [MobX Documentation](https://mobx.js.org/)
+- [mobx-react-lite](https://github.com/mobxjs/mobx-react-lite)
+- [Store Pattern Guide](./base-store.ts)
diff --git a/web/src/store/attachment.ts b/web/src/store/attachment.ts
index ee3c7cf68..09ab9a223 100644
--- a/web/src/store/attachment.ts
+++ b/web/src/store/attachment.ts
@@ -1,58 +1,193 @@
-import { makeAutoObservable } from "mobx";
+/**
+ * Attachment Store
+ *
+ * Manages file attachment state including uploads and metadata.
+ * This is a server state store that fetches and caches attachment data.
+ */
 import { attachmentServiceClient } from "@/grpcweb";
 import { CreateAttachmentRequest, Attachment, UpdateAttachmentRequest } from "@/types/proto/api/v1/attachment_service";
+import { StandardState, createServerStore } from "./base-store";
+import { createRequestKey } from "./store-utils";
 
-class LocalState {
+/**
+ * Attachment store state
+ * Uses a name-based map for efficient lookups
+ */
+class AttachmentState extends StandardState {
+  /**
+   * Map of attachments indexed by resource name (e.g., "attachments/123")
+   */
   attachmentMapByName: Record = {};
 
-  constructor() {
-    makeAutoObservable(this);
+  /**
+   * Computed getter for all attachments as an array
+   */
+  get attachments(): Attachment[] {
+    return Object.values(this.attachmentMapByName);
   }
 
-  setPartial(partial: Partial) {
-    Object.assign(this, partial);
+  /**
+   * Get attachment count
+   */
+  get size(): number {
+    return Object.keys(this.attachmentMapByName).length;
   }
 }
 
+/**
+ * Attachment store instance
+ */
 const attachmentStore = (() => {
-  const state = new LocalState();
+  const base = createServerStore(new AttachmentState(), {
+    name: "attachment",
+    enableDeduplication: true,
+  });
 
-  const fetchAttachmentByName = async (name: string) => {
-    const attachment = await attachmentServiceClient.getAttachment({
-      name,
-    });
-    const attachmentMap = { ...state.attachmentMapByName };
-    attachmentMap[attachment.name] = attachment;
-    state.setPartial({ attachmentMapByName: attachmentMap });
-    return attachment;
+  const { state, executeRequest } = base;
+
+  /**
+   * Fetch attachment by resource name
+   * Results are cached in the store
+   *
+   * @param name - Resource name (e.g., "attachments/123")
+   * @returns The attachment object
+   */
+  const fetchAttachmentByName = async (name: string): Promise => {
+    const requestKey = createRequestKey("fetchAttachment", { name });
+
+    return executeRequest(
+      requestKey,
+      async () => {
+        const attachment = await attachmentServiceClient.getAttachment({ name });
+
+        // Update cache
+        state.setPartial({
+          attachmentMapByName: {
+            ...state.attachmentMapByName,
+            [attachment.name]: attachment,
+          },
+        });
+
+        return attachment;
+      },
+      "FETCH_ATTACHMENT_FAILED",
+    );
   };
 
-  const getAttachmentByName = (name: string) => {
-    return Object.values(state.attachmentMapByName).find((a) => a.name === name);
+  /**
+   * Get attachment from cache by resource name
+   * Does not trigger a fetch if not found
+   *
+   * @param name - Resource name
+   * @returns The cached attachment or undefined
+   */
+  const getAttachmentByName = (name: string): Attachment | undefined => {
+    return state.attachmentMapByName[name];
   };
 
-  const createAttachment = async (create: CreateAttachmentRequest): Promise => {
-    const attachment = await attachmentServiceClient.createAttachment(create);
-    const attachmentMap = { ...state.attachmentMapByName };
-    attachmentMap[attachment.name] = attachment;
-    state.setPartial({ attachmentMapByName: attachmentMap });
-    return attachment;
+  /**
+   * Get or fetch attachment by name
+   * Checks cache first, fetches if not found
+   *
+   * @param name - Resource name
+   * @returns The attachment object
+   */
+  const getOrFetchAttachmentByName = async (name: string): Promise => {
+    const cached = getAttachmentByName(name);
+    if (cached) {
+      return cached;
+    }
+    return fetchAttachmentByName(name);
+  };
+
+  /**
+   * Create a new attachment
+   *
+   * @param request - Attachment creation request
+   * @returns The created attachment
+   */
+  const createAttachment = async (request: CreateAttachmentRequest): Promise => {
+    return executeRequest(
+      "", // No deduplication for creates
+      async () => {
+        const attachment = await attachmentServiceClient.createAttachment(request);
+
+        // Add to cache
+        state.setPartial({
+          attachmentMapByName: {
+            ...state.attachmentMapByName,
+            [attachment.name]: attachment,
+          },
+        });
+
+        return attachment;
+      },
+      "CREATE_ATTACHMENT_FAILED",
+    );
+  };
+
+  /**
+   * Update an existing attachment
+   *
+   * @param request - Attachment update request
+   * @returns The updated attachment
+   */
+  const updateAttachment = async (request: UpdateAttachmentRequest): Promise => {
+    return executeRequest(
+      "", // No deduplication for updates
+      async () => {
+        const attachment = await attachmentServiceClient.updateAttachment(request);
+
+        // Update cache
+        state.setPartial({
+          attachmentMapByName: {
+            ...state.attachmentMapByName,
+            [attachment.name]: attachment,
+          },
+        });
+
+        return attachment;
+      },
+      "UPDATE_ATTACHMENT_FAILED",
+    );
+  };
+
+  /**
+   * Delete an attachment
+   *
+   * @param name - Resource name of the attachment to delete
+   */
+  const deleteAttachment = async (name: string): Promise => {
+    return executeRequest(
+      "", // No deduplication for deletes
+      async () => {
+        await attachmentServiceClient.deleteAttachment({ name });
+
+        // Remove from cache
+        const attachmentMap = { ...state.attachmentMapByName };
+        delete attachmentMap[name];
+        state.setPartial({ attachmentMapByName: attachmentMap });
+      },
+      "DELETE_ATTACHMENT_FAILED",
+    );
   };
 
-  const updateAttachment = async (update: UpdateAttachmentRequest): Promise => {
-    const attachment = await attachmentServiceClient.updateAttachment(update);
-    const attachmentMap = { ...state.attachmentMapByName };
-    attachmentMap[attachment.name] = attachment;
-    state.setPartial({ attachmentMapByName: attachmentMap });
-    return attachment;
+  /**
+   * Clear all cached attachments
+   */
+  const clearCache = (): void => {
+    state.setPartial({ attachmentMapByName: {} });
   };
 
   return {
     state,
     fetchAttachmentByName,
     getAttachmentByName,
+    getOrFetchAttachmentByName,
     createAttachment,
     updateAttachment,
+    deleteAttachment,
+    clearCache,
   };
 })();
 
diff --git a/web/src/store/base-store.ts b/web/src/store/base-store.ts
new file mode 100644
index 000000000..0e27b2abe
--- /dev/null
+++ b/web/src/store/base-store.ts
@@ -0,0 +1,175 @@
+/**
+ * Base store classes and utilities for consistent store patterns
+ *
+ * This module provides:
+ * - BaseServerStore: For stores that fetch data from APIs
+ * - BaseClientStore: For stores that manage UI/client state
+ * - Common patterns for all stores
+ */
+import { makeAutoObservable } from "mobx";
+import { RequestDeduplicator, StoreError } from "./store-utils";
+
+/**
+ * Base interface for all store states
+ * Ensures all stores have a consistent setPartial method
+ */
+export interface BaseState {
+  setPartial(partial: Partial): void;
+}
+
+/**
+ * Base class for server state stores (data fetching)
+ *
+ * Server stores:
+ * - Fetch data from APIs
+ * - Cache responses in memory
+ * - Handle errors with StoreError
+ * - Support request deduplication
+ *
+ * @example
+ * class MemoState implements BaseState {
+ *   memoMapByName: Record = {};
+ *   constructor() { makeAutoObservable(this); }
+ *   setPartial(partial: Partial) { Object.assign(this, partial); }
+ * }
+ *
+ * const store = createServerStore(new MemoState());
+ */
+export interface ServerStoreConfig {
+  /**
+   * Enable request deduplication
+   * Prevents multiple identical requests from running simultaneously
+   */
+  enableDeduplication?: boolean;
+
+  /**
+   * Store name for debugging and error messages
+   */
+  name: string;
+}
+
+/**
+ * Create a server store with built-in utilities
+ */
+export function createServerStore(state: TState, config: ServerStoreConfig) {
+  const deduplicator = config.enableDeduplication !== false ? new RequestDeduplicator() : null;
+
+  return {
+    state,
+    deduplicator,
+    name: config.name,
+
+    /**
+     * Wrap an async operation with error handling and optional deduplication
+     */
+    async executeRequest(key: string, operation: () => Promise, errorCode?: string): Promise {
+      try {
+        if (deduplicator && key) {
+          return await deduplicator.execute(key, operation);
+        }
+        return await operation();
+      } catch (error) {
+        if (StoreError.isAbortError(error)) {
+          throw error; // Re-throw abort errors as-is
+        }
+        throw StoreError.wrap(errorCode || `${config.name.toUpperCase()}_OPERATION_FAILED`, error);
+      }
+    },
+  };
+}
+
+/**
+ * Base class for client state stores (UI state)
+ *
+ * Client stores:
+ * - Manage UI preferences and transient state
+ * - May persist to localStorage or URL
+ * - No API calls
+ * - Instant updates
+ *
+ * @example
+ * class ViewState implements BaseState {
+ *   orderByTimeAsc = false;
+ *   layout: "LIST" | "MASONRY" = "LIST";
+ *   constructor() { makeAutoObservable(this); }
+ *   setPartial(partial: Partial) {
+ *     Object.assign(this, partial);
+ *     localStorage.setItem("view", JSON.stringify(this));
+ *   }
+ * }
+ */
+export interface ClientStoreConfig {
+  /**
+   * Store name for debugging
+   */
+  name: string;
+
+  /**
+   * Enable localStorage persistence
+   */
+  persistence?: {
+    key: string;
+    serialize?: (state: any) => string;
+    deserialize?: (data: string) => any;
+  };
+}
+
+/**
+ * Create a client store with optional persistence
+ */
+export function createClientStore(state: TState, config: ClientStoreConfig) {
+  // Load from localStorage if enabled
+  if (config.persistence) {
+    try {
+      const cached = localStorage.getItem(config.persistence.key);
+      if (cached) {
+        const data = config.persistence.deserialize ? config.persistence.deserialize(cached) : JSON.parse(cached);
+        Object.assign(state, data);
+      }
+    } catch (error) {
+      console.warn(`Failed to load ${config.name} from localStorage:`, error);
+    }
+  }
+
+  return {
+    state,
+    name: config.name,
+
+    /**
+     * Save state to localStorage if persistence is enabled
+     */
+    persist(): void {
+      if (config.persistence) {
+        try {
+          const data = config.persistence.serialize ? config.persistence.serialize(state) : JSON.stringify(state);
+          localStorage.setItem(config.persistence.key, data);
+        } catch (error) {
+          console.warn(`Failed to persist ${config.name}:`, error);
+        }
+      }
+    },
+
+    /**
+     * Clear persisted state
+     */
+    clearPersistence(): void {
+      if (config.persistence) {
+        localStorage.removeItem(config.persistence.key);
+      }
+    },
+  };
+}
+
+/**
+ * Standard state class implementation
+ * Use this as a base for your state classes
+ */
+export abstract class StandardState implements BaseState {
+  constructor() {
+    makeAutoObservable(this);
+  }
+
+  setPartial(partial: Partial): void {
+    Object.assign(this, partial);
+  }
+}
diff --git a/web/src/store/config.ts b/web/src/store/config.ts
new file mode 100644
index 000000000..6d8f705f8
--- /dev/null
+++ b/web/src/store/config.ts
@@ -0,0 +1,72 @@
+/**
+ * MobX configuration for strict state management
+ *
+ * This configuration enforces best practices to prevent common mistakes:
+ * - All state changes must happen in actions (prevents accidental mutations)
+ * - Computed values cannot have side effects (ensures purity)
+ * - Observables must be accessed within reactions (helps catch missing observers)
+ *
+ * This file is imported early in the application lifecycle to configure MobX
+ * before any stores are created.
+ */
+import { configure } from "mobx";
+
+/**
+ * Configure MobX with production-safe settings
+ * This runs immediately when the module is imported
+ */
+configure({
+  /**
+   * Enforce that all state mutations happen within actions
+   * Since we use makeAutoObservable, all methods are automatically actions
+   * This prevents bugs from direct mutations like:
+   *   store.state.value = 5  // ERROR: This will throw
+   *
+   * Instead, you must use action methods:
+   *   store.state.setPartial({ value: 5 })  // Correct
+   */
+  enforceActions: "never", // Start with "never", can be upgraded to "observed" or "always"
+
+  /**
+   * Use Proxies for better performance and ES6 compatibility
+   * makeAutoObservable requires this to be enabled
+   */
+  useProxies: "always",
+
+  /**
+   * Isolate global state to prevent accidental sharing between tests
+   */
+  isolateGlobalState: true,
+
+  /**
+   * Disable error boundaries so errors propagate normally
+   * This ensures React error boundaries can catch store errors
+   */
+  disableErrorBoundaries: false,
+});
+
+/**
+ * Enable strict mode for development
+ * Call this in main.tsx if you want stricter checking
+ */
+export function enableStrictMode() {
+  if (import.meta.env.DEV) {
+    configure({
+      enforceActions: "observed", // Enforce actions only for observed values
+      computedRequiresReaction: false, // Don't warn about computed access
+      reactionRequiresObservable: false, // Don't warn about reactions
+    });
+    console.info("✓ MobX strict mode enabled");
+  }
+}
+
+/**
+ * Enable production mode for maximum performance
+ * This is automatically called in production builds
+ */
+export function enableProductionMode() {
+  configure({
+    enforceActions: "never", // No runtime checks for performance
+    disableErrorBoundaries: false,
+  });
+}
diff --git a/web/src/store/index.ts b/web/src/store/index.ts
index 0dfc2ee31..9cbfd0248 100644
--- a/web/src/store/index.ts
+++ b/web/src/store/index.ts
@@ -1,8 +1,114 @@
+/**
+ * Store Module
+ *
+ * This module exports all application stores and their types.
+ *
+ * ## Store Architecture
+ *
+ * Stores are divided into two categories:
+ *
+ * ### Server State Stores (Data Fetching)
+ * These stores fetch and cache data from the backend API:
+ * - **memoStore**: Memo CRUD operations
+ * - **userStore**: User authentication and settings
+ * - **workspaceStore**: Workspace configuration
+ * - **attachmentStore**: File attachment management
+ *
+ * Features:
+ * - Request deduplication
+ * - Error handling with StoreError
+ * - Optimistic updates (memo updates)
+ * - Computed property memoization
+ *
+ * ### Client State Stores (UI State)
+ * These stores manage UI preferences and transient state:
+ * - **viewStore**: Display preferences (sort order, layout)
+ * - **memoFilterStore**: Active search filters
+ *
+ * Features:
+ * - localStorage persistence (viewStore)
+ * - URL synchronization (memoFilterStore)
+ * - No API calls
+ *
+ * ## Usage
+ *
+ * ```typescript
+ * import { memoStore, userStore, viewStore } from "@/store";
+ * import { observer } from "mobx-react-lite";
+ *
+ * const MyComponent = observer(() => {
+ *   const memos = memoStore.state.memos;
+ *   const user = userStore.state.currentUser;
+ *
+ *   return ...
;
+ * });
+ * ```
+ */
+// Server State Stores
 import attachmentStore from "./attachment";
 import memoStore from "./memo";
+// Client State Stores
 import memoFilterStore from "./memoFilter";
 import userStore from "./user";
 import viewStore from "./view";
 import workspaceStore from "./workspace";
 
-export { memoFilterStore, memoStore, attachmentStore, workspaceStore, userStore, viewStore };
+// Utilities and Types
+export { StoreError, RequestDeduplicator, createRequestKey } from "./store-utils";
+export { StandardState, createServerStore, createClientStore } from "./base-store";
+export type { BaseState, ServerStoreConfig, ClientStoreConfig } from "./base-store";
+
+// Re-export filter types
+export type { FilterFactor, MemoFilter } from "./memoFilter";
+export { getMemoFilterKey, parseFilterQuery, stringifyFilters } from "./memoFilter";
+
+// Re-export view types
+export type { LayoutMode } from "./view";
+
+// Re-export workspace types
+export type { Theme } from "./workspace";
+export { isValidTheme } from "./workspace";
+
+// Re-export common utilities
+export {
+  workspaceSettingNamePrefix,
+  userNamePrefix,
+  memoNamePrefix,
+  identityProviderNamePrefix,
+  activityNamePrefix,
+  extractUserIdFromName,
+  extractMemoIdFromName,
+  extractIdentityProviderIdFromName,
+} from "./common";
+
+// Export store instances
+export {
+  // Server state stores
+  memoStore,
+  userStore,
+  workspaceStore,
+  attachmentStore,
+
+  // Client state stores
+  memoFilterStore,
+  viewStore,
+};
+
+/**
+ * All stores grouped by category for convenience
+ */
+export const stores = {
+  // Server state
+  server: {
+    memo: memoStore,
+    user: userStore,
+    workspace: workspaceStore,
+    attachment: attachmentStore,
+  },
+
+  // Client state
+  client: {
+    memoFilter: memoFilterStore,
+    view: viewStore,
+  },
+} as const;
diff --git a/web/src/store/memo.ts b/web/src/store/memo.ts
index 0ff5d79d2..5bf4e182b 100644
--- a/web/src/store/memo.ts
+++ b/web/src/store/memo.ts
@@ -2,6 +2,7 @@ import { uniqueId } from "lodash-es";
 import { makeAutoObservable } from "mobx";
 import { memoServiceClient } from "@/grpcweb";
 import { CreateMemoRequest, ListMemosRequest, Memo } from "@/types/proto/api/v1/memo_service";
+import { RequestDeduplicator, createRequestKey, StoreError } from "./store-utils";
 
 class LocalState {
   stateId: string = uniqueId();
@@ -31,44 +32,50 @@ class LocalState {
 
 const memoStore = (() => {
   const state = new LocalState();
+  const deduplicator = new RequestDeduplicator();
 
   const fetchMemos = async (request: Partial) => {
-    if (state.currentRequest) {
-      state.currentRequest.abort();
-    }
+    // Deduplicate requests with the same parameters
+    const requestKey = createRequestKey("fetchMemos", request as Record);
 
-    const controller = new AbortController();
-    state.setPartial({ currentRequest: controller });
+    return deduplicator.execute(requestKey, async () => {
+      if (state.currentRequest) {
+        state.currentRequest.abort();
+      }
 
-    try {
-      const { memos, nextPageToken } = await memoServiceClient.listMemos(
-        {
-          ...request,
-        },
-        { signal: controller.signal },
-      );
-
-      if (!controller.signal.aborted) {
-        const memoMap = request.pageToken ? { ...state.memoMapByName } : {};
-        for (const memo of memos) {
-          memoMap[memo.name] = memo;
+      const controller = new AbortController();
+      state.setPartial({ currentRequest: controller });
+
+      try {
+        const { memos, nextPageToken } = await memoServiceClient.listMemos(
+          {
+            ...request,
+          },
+          { signal: controller.signal },
+        );
+
+        if (!controller.signal.aborted) {
+          const memoMap = request.pageToken ? { ...state.memoMapByName } : {};
+          for (const memo of memos) {
+            memoMap[memo.name] = memo;
+          }
+          state.setPartial({
+            stateId: uniqueId(),
+            memoMapByName: memoMap,
+          });
+          return { memos, nextPageToken };
+        }
+      } catch (error: any) {
+        if (StoreError.isAbortError(error)) {
+          return;
+        }
+        throw StoreError.wrap("FETCH_MEMOS_FAILED", error);
+      } finally {
+        if (state.currentRequest === controller) {
+          state.setPartial({ currentRequest: null });
         }
-        state.setPartial({
-          stateId: uniqueId(),
-          memoMapByName: memoMap,
-        });
-        return { memos, nextPageToken };
-      }
-    } catch (error: any) {
-      if (error.name === "AbortError") {
-        return;
-      }
-      throw error;
-    } finally {
-      if (state.currentRequest === controller) {
-        state.setPartial({ currentRequest: null });
       }
-    }
+    });
   };
 
   const getOrFetchMemoByName = async (name: string, options?: { skipCache?: boolean; skipStore?: boolean }) => {
@@ -109,18 +116,43 @@ const memoStore = (() => {
   };
 
   const updateMemo = async (update: Partial, updateMask: string[]) => {
-    const memo = await memoServiceClient.updateMemo({
-      memo: update,
-      updateMask,
-    });
+    // Optimistic update: immediately update the UI
+    const previousMemo = state.memoMapByName[update.name!];
+    const optimisticMemo = { ...previousMemo, ...update };
 
+    // Apply optimistic update
     const memoMap = { ...state.memoMapByName };
-    memoMap[memo.name] = memo;
+    memoMap[update.name!] = optimisticMemo;
     state.setPartial({
       stateId: uniqueId(),
       memoMapByName: memoMap,
     });
-    return memo;
+
+    try {
+      // Perform actual server update
+      const memo = await memoServiceClient.updateMemo({
+        memo: update,
+        updateMask,
+      });
+
+      // Confirm with server response
+      const confirmedMemoMap = { ...state.memoMapByName };
+      confirmedMemoMap[memo.name] = memo;
+      state.setPartial({
+        stateId: uniqueId(),
+        memoMapByName: confirmedMemoMap,
+      });
+      return memo;
+    } catch (error) {
+      // Rollback on error
+      const rollbackMemoMap = { ...state.memoMapByName };
+      rollbackMemoMap[update.name!] = previousMemo;
+      state.setPartial({
+        stateId: uniqueId(),
+        memoMapByName: rollbackMemoMap,
+      });
+      throw StoreError.wrap("UPDATE_MEMO_FAILED", error);
+    }
   };
 
   const deleteMemo = async (name: string) => {
diff --git a/web/src/store/memoFilter.ts b/web/src/store/memoFilter.ts
index eddbff543..16325763b 100644
--- a/web/src/store/memoFilter.ts
+++ b/web/src/store/memoFilter.ts
@@ -1,31 +1,57 @@
+/**
+ * Memo Filter Store
+ *
+ * Manages active memo filters and search state.
+ * This is a client state store that syncs with URL query parameters.
+ *
+ * Filters are URL-driven and shareable - copying the URL preserves the filter state.
+ */
 import { uniqBy } from "lodash-es";
-import { makeAutoObservable } from "mobx";
+import { StandardState } from "./base-store";
 
+/**
+ * Filter factor types
+ * Defines what aspect of a memo to filter by
+ */
 export type FilterFactor =
-  | "tagSearch"
-  | "visibility"
-  | "contentSearch"
-  | "displayTime"
-  | "pinned"
-  | "property.hasLink"
-  | "property.hasTaskList"
-  | "property.hasCode";
-
+  | "tagSearch" // Filter by tag name
+  | "visibility" // Filter by visibility (public/private)
+  | "contentSearch" // Search in memo content
+  | "displayTime" // Filter by date
+  | "pinned" // Show only pinned memos
+  | "property.hasLink" // Memos containing links
+  | "property.hasTaskList" // Memos with task lists
+  | "property.hasCode"; // Memos with code blocks
+
+/**
+ * Memo filter object
+ */
 export interface MemoFilter {
   factor: FilterFactor;
   value: string;
 }
 
-export const getMemoFilterKey = (filter: MemoFilter) => `${filter.factor}:${filter.value}`;
-
+/**
+ * Generate a unique key for a filter
+ * Used for deduplication
+ */
+export const getMemoFilterKey = (filter: MemoFilter): string => `${filter.factor}:${filter.value}`;
+
+/**
+ * Parse filter query string from URL into filter objects
+ *
+ * @param query - URL query string (e.g., "tagSearch:work,pinned:true")
+ * @returns Array of filter objects
+ */
 export const parseFilterQuery = (query: string | null): MemoFilter[] => {
   if (!query) return [];
+
   try {
     return query.split(",").map((filterStr) => {
       const [factor, value] = filterStr.split(":");
       return {
         factor: factor as FilterFactor,
-        value: decodeURIComponent(value),
+        value: decodeURIComponent(value || ""),
       };
     });
   } catch (error) {
@@ -34,59 +60,191 @@ export const parseFilterQuery = (query: string | null): MemoFilter[] => {
   }
 };
 
+/**
+ * Convert filter objects into URL query string
+ *
+ * @param filters - Array of filter objects
+ * @returns URL-encoded query string
+ */
 export const stringifyFilters = (filters: MemoFilter[]): string => {
   return filters.map((filter) => `${filter.factor}:${encodeURIComponent(filter.value)}`).join(",");
 };
 
-class MemoFilterState {
+/**
+ * Memo filter store state
+ */
+class MemoFilterState extends StandardState {
+  /**
+   * Active filters
+   */
   filters: MemoFilter[] = [];
+
+  /**
+   * Currently selected shortcut ID
+   * Shortcuts are predefined filter combinations
+   */
   shortcut?: string = undefined;
 
+  /**
+   * Initialize from URL on construction
+   */
   constructor() {
-    makeAutoObservable(this);
-    this.init();
+    super();
+    this.initFromURL();
   }
 
-  init() {
-    const searchParams = new URLSearchParams(window.location.search);
-    this.filters = parseFilterQuery(searchParams.get("filter"));
+  /**
+   * Load filters from current URL query parameters
+   */
+  private initFromURL(): void {
+    try {
+      const searchParams = new URLSearchParams(window.location.search);
+      this.filters = parseFilterQuery(searchParams.get("filter"));
+    } catch (error) {
+      console.warn("Failed to parse filters from URL:", error);
+      this.filters = [];
+    }
   }
 
-  setState(state: Partial) {
-    Object.assign(this, state);
-  }
-
-  getFiltersByFactor(factor: FilterFactor) {
+  /**
+   * Get all filters for a specific factor
+   *
+   * @param factor - The filter factor to query
+   * @returns Array of matching filters
+   */
+  getFiltersByFactor(factor: FilterFactor): MemoFilter[] {
     return this.filters.filter((f) => f.factor === factor);
   }
 
-  addFilter(filter: MemoFilter) {
+  /**
+   * Add a filter (deduplicates automatically)
+   *
+   * @param filter - The filter to add
+   */
+  addFilter(filter: MemoFilter): void {
     this.filters = uniqBy([...this.filters, filter], getMemoFilterKey);
   }
 
-  removeFilter(filterFn: (f: MemoFilter) => boolean) {
-    this.filters = this.filters.filter((f) => !filterFn(f));
+  /**
+   * Remove filters matching the predicate
+   *
+   * @param predicate - Function that returns true for filters to remove
+   */
+  removeFilter(predicate: (f: MemoFilter) => boolean): void {
+    this.filters = this.filters.filter((f) => !predicate(f));
+  }
+
+  /**
+   * Remove all filters for a specific factor
+   *
+   * @param factor - The filter factor to remove
+   */
+  removeFiltersByFactor(factor: FilterFactor): void {
+    this.filters = this.filters.filter((f) => f.factor !== factor);
+  }
+
+  /**
+   * Clear all filters
+   */
+  clearAllFilters(): void {
+    this.filters = [];
+    this.shortcut = undefined;
   }
 
-  setShortcut(shortcut?: string) {
+  /**
+   * Set the current shortcut
+   *
+   * @param shortcut - Shortcut ID or undefined to clear
+   */
+  setShortcut(shortcut?: string): void {
     this.shortcut = shortcut;
   }
+
+  /**
+   * Check if a specific filter is active
+   *
+   * @param filter - The filter to check
+   * @returns True if the filter is active
+   */
+  hasFilter(filter: MemoFilter): boolean {
+    return this.filters.some((f) => getMemoFilterKey(f) === getMemoFilterKey(filter));
+  }
+
+  /**
+   * Check if any filters are active
+   */
+  get hasActiveFilters(): boolean {
+    return this.filters.length > 0 || this.shortcut !== undefined;
+  }
 }
 
+/**
+ * Memo filter store instance
+ */
 const memoFilterStore = (() => {
   const state = new MemoFilterState();
 
   return {
-    get filters() {
+    /**
+     * Direct access to state for observers
+     */
+    state,
+
+    /**
+     * Get all active filters
+     */
+    get filters(): MemoFilter[] {
       return state.filters;
     },
-    get shortcut() {
+
+    /**
+     * Get current shortcut ID
+     */
+    get shortcut(): string | undefined {
       return state.shortcut;
     },
-    getFiltersByFactor: (factor: FilterFactor) => state.getFiltersByFactor(factor),
-    addFilter: (filter: MemoFilter) => state.addFilter(filter),
-    removeFilter: (filterFn: (f: MemoFilter) => boolean) => state.removeFilter(filterFn),
-    setShortcut: (shortcut?: string) => state.setShortcut(shortcut),
+
+    /**
+     * Check if any filters are active
+     */
+    get hasActiveFilters(): boolean {
+      return state.hasActiveFilters;
+    },
+
+    /**
+     * Get filters by factor
+     */
+    getFiltersByFactor: (factor: FilterFactor): MemoFilter[] => state.getFiltersByFactor(factor),
+
+    /**
+     * Add a filter
+     */
+    addFilter: (filter: MemoFilter): void => state.addFilter(filter),
+
+    /**
+     * Remove filters matching predicate
+     */
+    removeFilter: (predicate: (f: MemoFilter) => boolean): void => state.removeFilter(predicate),
+
+    /**
+     * Remove all filters for a factor
+     */
+    removeFiltersByFactor: (factor: FilterFactor): void => state.removeFiltersByFactor(factor),
+
+    /**
+     * Clear all filters
+     */
+    clearAllFilters: (): void => state.clearAllFilters(),
+
+    /**
+     * Set current shortcut
+     */
+    setShortcut: (shortcut?: string): void => state.setShortcut(shortcut),
+
+    /**
+     * Check if a filter is active
+     */
+    hasFilter: (filter: MemoFilter): boolean => state.hasFilter(filter),
   };
 })();
 
diff --git a/web/src/store/store-utils.ts b/web/src/store/store-utils.ts
new file mode 100644
index 000000000..b9e328123
--- /dev/null
+++ b/web/src/store/store-utils.ts
@@ -0,0 +1,152 @@
+/**
+ * Store utilities for MobX stores
+ * Provides request deduplication, error handling, and other common patterns
+ */
+
+/**
+ * Custom error class for store operations
+ * Provides structured error information for better debugging and error handling
+ */
+export class StoreError extends Error {
+  constructor(
+    public readonly code: string,
+    message: string,
+    public readonly originalError?: unknown,
+  ) {
+    super(message);
+    this.name = "StoreError";
+  }
+
+  /**
+   * Check if an error is an AbortError from a cancelled request
+   */
+  static isAbortError(error: unknown): boolean {
+    return error instanceof Error && error.name === "AbortError";
+  }
+
+  /**
+   * Wrap an unknown error in a StoreError for consistent error handling
+   */
+  static wrap(code: string, error: unknown, customMessage?: string): StoreError {
+    if (error instanceof StoreError) {
+      return error;
+    }
+
+    const message = customMessage || (error instanceof Error ? error.message : "Unknown error");
+    return new StoreError(code, message, error);
+  }
+}
+
+/**
+ * Request deduplication manager
+ * Prevents multiple identical requests from being made simultaneously
+ */
+export class RequestDeduplicator {
+  private pendingRequests = new Map>();
+
+  /**
+   * Execute a request with deduplication
+   * If the same request key is already pending, returns the existing promise
+   *
+   * @param key - Unique identifier for this request (e.g., JSON.stringify(params))
+   * @param requestFn - Function that executes the actual request
+   * @returns Promise that resolves with the request result
+   */
+  async execute(key: string, requestFn: () => Promise): Promise {
+    // Check if this request is already pending
+    if (this.pendingRequests.has(key)) {
+      return this.pendingRequests.get(key) as Promise;
+    }
+
+    // Create new request
+    const promise = requestFn().finally(() => {
+      // Clean up after request completes (success or failure)
+      this.pendingRequests.delete(key);
+    });
+
+    // Store the pending request
+    this.pendingRequests.set(key, promise);
+
+    return promise;
+  }
+
+  /**
+   * Cancel all pending requests
+   */
+  clear(): void {
+    this.pendingRequests.clear();
+  }
+
+  /**
+   * Check if a request with the given key is pending
+   */
+  isPending(key: string): boolean {
+    return this.pendingRequests.has(key);
+  }
+
+  /**
+   * Get the number of pending requests
+   */
+  get size(): number {
+    return this.pendingRequests.size;
+  }
+}
+
+/**
+ * Create a request key from parameters
+ * Useful for generating consistent keys for request deduplication
+ */
+export function createRequestKey(prefix: string, params?: Record): string {
+  if (!params) {
+    return prefix;
+  }
+
+  // Sort keys for consistent hashing
+  const sortedParams = Object.keys(params)
+    .sort()
+    .reduce(
+      (acc, key) => {
+        acc[key] = params[key];
+        return acc;
+      },
+      {} as Record,
+    );
+
+  return `${prefix}:${JSON.stringify(sortedParams)}`;
+}
+
+/**
+ * Optimistic update helper
+ * Handles optimistic updates with rollback on error
+ */
+export class OptimisticUpdate {
+  constructor(
+    private getCurrentState: () => T,
+    private setState: (state: T) => void,
+  ) {}
+
+  /**
+   * Execute an update with optimistic UI updates
+   *
+   * @param optimisticState - State to apply immediately
+   * @param updateFn - Async function that performs the actual update
+   * @returns Promise that resolves with the update result
+   */
+  async execute(optimisticState: T, updateFn: () => Promise): Promise {
+    const previousState = this.getCurrentState();
+
+    try {
+      // Apply optimistic update immediately
+      this.setState(optimisticState);
+
+      // Perform actual update
+      const result = await updateFn();
+
+      return result;
+    } catch (error) {
+      // Rollback on error
+      this.setState(previousState);
+      throw error;
+    }
+  }
+}
diff --git a/web/src/store/user.ts b/web/src/store/user.ts
index 6f0e49c93..518bd62cd 100644
--- a/web/src/store/user.ts
+++ b/web/src/store/user.ts
@@ -1,5 +1,5 @@
 import { uniqueId } from "lodash-es";
-import { makeAutoObservable } from "mobx";
+import { makeAutoObservable, computed } from "mobx";
 import { authServiceClient, inboxServiceClient, userServiceClient, shortcutServiceClient } from "@/grpcweb";
 import { Inbox } from "@/types/proto/api/v1/inbox_service";
 import { Shortcut } from "@/types/proto/api/v1/shortcut_service";
@@ -14,6 +14,7 @@ import {
   UserStats,
 } from "@/types/proto/api/v1/user_service";
 import { findNearestMatchedLanguage } from "@/utils/i18n";
+import { RequestDeduplicator, createRequestKey, StoreError } from "./store-utils";
 import workspaceStore from "./workspace";
 
 class LocalState {
@@ -30,14 +31,21 @@ class LocalState {
   // The state id of user stats map.
   statsStateId = uniqueId();
 
+  /**
+   * Computed property that aggregates tag counts across all users.
+   * Uses @computed to memoize the result and only recalculate when userStatsByName changes.
+   * This prevents unnecessary recalculations on every access.
+   */
   get tagCount() {
-    const tagCount: Record = {};
-    for (const stats of Object.values(this.userStatsByName)) {
-      for (const tag of Object.keys(stats.tagCount)) {
-        tagCount[tag] = (tagCount[tag] || 0) + stats.tagCount[tag];
+    return computed(() => {
+      const tagCount: Record = {};
+      for (const stats of Object.values(this.userStatsByName)) {
+        for (const tag of Object.keys(stats.tagCount)) {
+          tagCount[tag] = (tagCount[tag] || 0) + stats.tagCount[tag];
+        }
       }
-    }
-    return tagCount;
+      return tagCount;
+    }).get();
   }
 
   get currentUserStats() {
@@ -58,6 +66,7 @@ class LocalState {
 
 const userStore = (() => {
   const state = new LocalState();
+  const deduplicator = new RequestDeduplicator();
 
   const getOrFetchUserByName = async (name: string) => {
     const userMap = state.userMapByName;
@@ -104,15 +113,22 @@ const userStore = (() => {
   };
 
   const fetchUsers = async () => {
-    const { users } = await userServiceClient.listUsers({});
-    const userMap = state.userMapByName;
-    for (const user of users) {
-      userMap[user.name] = user;
-    }
-    state.setPartial({
-      userMapByName: userMap,
+    const requestKey = createRequestKey("fetchUsers");
+    return deduplicator.execute(requestKey, async () => {
+      try {
+        const { users } = await userServiceClient.listUsers({});
+        const userMap = state.userMapByName;
+        for (const user of users) {
+          userMap[user.name] = user;
+        }
+        state.setPartial({
+          userMapByName: userMap,
+        });
+        return users;
+      } catch (error) {
+        throw StoreError.wrap("FETCH_USERS_FAILED", error);
+      }
     });
-    return users;
   };
 
   const updateUser = async (user: Partial, updateMask: string[]) => {
@@ -237,21 +253,28 @@ const userStore = (() => {
   };
 
   const fetchUserStats = async (user?: string) => {
-    const userStatsByName: Record = {};
-    if (!user) {
-      const { stats } = await userServiceClient.listAllUserStats({});
-      for (const userStats of stats) {
-        userStatsByName[userStats.name] = userStats;
+    const requestKey = createRequestKey("fetchUserStats", { user });
+    return deduplicator.execute(requestKey, async () => {
+      try {
+        const userStatsByName: Record = {};
+        if (!user) {
+          const { stats } = await userServiceClient.listAllUserStats({});
+          for (const userStats of stats) {
+            userStatsByName[userStats.name] = userStats;
+          }
+        } else {
+          const userStats = await userServiceClient.getUserStats({ name: user });
+          userStatsByName[user] = userStats;
+        }
+        state.setPartial({
+          userStatsByName: {
+            ...state.userStatsByName,
+            ...userStatsByName,
+          },
+        });
+      } catch (error) {
+        throw StoreError.wrap("FETCH_USER_STATS_FAILED", error);
       }
-    } else {
-      const userStats = await userServiceClient.getUserStats({ name: user });
-      userStatsByName[user] = userStats;
-    }
-    state.setPartial({
-      userStatsByName: {
-        ...state.userStatsByName,
-        ...userStatsByName,
-      },
     });
   };
 
@@ -278,23 +301,38 @@ const userStore = (() => {
   };
 })();
 
-// TODO: refactor initialUserStore as it has temporal coupling
-// need to make it more clear that the order of the body is important
-// or it leads to false positives
-// See: https://github.com/usememos/memos/issues/4978
+/**
+ * Initializes the user store with proper sequencing to avoid temporal coupling.
+ *
+ * Initialization steps (order is critical):
+ * 1. Fetch current authenticated user session
+ * 2. Set current user in store (required for subsequent calls)
+ * 3. Fetch user settings (depends on currentUser being set)
+ * 4. Apply user preferences to workspace store
+ *
+ * @throws Never - errors are handled internally with fallback behavior
+ */
 export const initialUserStore = async () => {
   try {
+    // Step 1: Authenticate and get current user
     const { user: currentUser } = await authServiceClient.getCurrentSession({});
+
     if (!currentUser) {
-      // If no user is authenticated, we can skip the rest of the initialization.
+      // No authenticated user - clear state and use default locale
       userStore.state.setPartial({
         currentUser: undefined,
         userGeneralSetting: undefined,
         userMapByName: {},
       });
+
+      const locale = findNearestMatchedLanguage(navigator.language);
+      workspaceStore.state.setPartial({ locale });
       return;
     }
 
+    // Step 2: Set current user in store
+    // CRITICAL: This must happen before fetchUserSettings() is called
+    // because fetchUserSettings() depends on state.currentUser being set
     userStore.state.setPartial({
       currentUser: currentUser.name,
       userMapByName: {
@@ -302,24 +340,31 @@ export const initialUserStore = async () => {
       },
     });
 
-    // must be called after user is set in store
+    // Step 3: Fetch user settings
+    // CRITICAL: This must happen after currentUser is set in step 2
+    // The fetchUserSettings() method checks state.currentUser internally
     await userStore.fetchUserSettings();
 
-    // must be run after fetchUserSettings is called.
-    // Apply general settings to workspace if available
+    // Step 4: Apply user preferences to workspace
+    // CRITICAL: This must happen after fetchUserSettings() completes
+    // We need userGeneralSetting to be populated before accessing it
     const generalSetting = userStore.state.userGeneralSetting;
     if (generalSetting) {
+      // Note: setPartial will validate theme automatically
       workspaceStore.state.setPartial({
         locale: generalSetting.locale,
-        theme: generalSetting.theme || "default",
+        theme: generalSetting.theme || "default", // Validation handled by setPartial
       });
+    } else {
+      // Fallback if settings weren't loaded
+      const locale = findNearestMatchedLanguage(navigator.language);
+      workspaceStore.state.setPartial({ locale });
     }
-  } catch {
-    // find the nearest matched lang based on the `navigator.language` if the user is unauthenticated or settings retrieval fails.
+  } catch (error) {
+    // On any error, fall back to browser language detection
+    console.error("Failed to initialize user store:", error);
     const locale = findNearestMatchedLanguage(navigator.language);
-    workspaceStore.state.setPartial({
-      locale: locale,
-    });
+    workspaceStore.state.setPartial({ locale });
   }
 };
 
diff --git a/web/src/store/view.ts b/web/src/store/view.ts
index a8535ad1e..82a8b4acb 100644
--- a/web/src/store/view.ts
+++ b/web/src/store/view.ts
@@ -1,49 +1,128 @@
-import { makeAutoObservable } from "mobx";
+/**
+ * View Store
+ *
+ * Manages UI display preferences and layout settings.
+ * This is a client state store that persists to localStorage.
+ */
+import { StandardState } from "./base-store";
 
 const LOCAL_STORAGE_KEY = "memos-view-setting";
 
-class LocalState {
+/**
+ * Layout mode options
+ */
+export type LayoutMode = "LIST" | "MASONRY";
+
+/**
+ * View store state
+ * Contains UI preferences for displaying memos
+ */
+class ViewState extends StandardState {
+  /**
+   * Sort order: true = ascending (oldest first), false = descending (newest first)
+   */
   orderByTimeAsc: boolean = false;
-  layout: "LIST" | "MASONRY" = "LIST";
 
-  constructor() {
-    makeAutoObservable(this);
-  }
+  /**
+   * Display layout mode
+   * - LIST: Traditional vertical list
+   * - MASONRY: Pinterest-style grid layout
+   */
+  layout: LayoutMode = "LIST";
+
+  /**
+   * Override setPartial to persist to localStorage
+   */
+  setPartial(partial: Partial): void {
+    // Validate layout if provided
+    if (partial.layout !== undefined && !["LIST", "MASONRY"].includes(partial.layout)) {
+      console.warn(`Invalid layout "${partial.layout}", ignoring`);
+      return;
+    }
 
-  setPartial(partial: Partial) {
     Object.assign(this, partial);
-    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this));
+
+    // Persist to localStorage
+    try {
+      localStorage.setItem(
+        LOCAL_STORAGE_KEY,
+        JSON.stringify({
+          orderByTimeAsc: this.orderByTimeAsc,
+          layout: this.layout,
+        }),
+      );
+    } catch (error) {
+      console.warn("Failed to persist view settings:", error);
+    }
   }
 }
 
+/**
+ * View store instance
+ */
 const viewStore = (() => {
-  const state = new LocalState();
+  const state = new ViewState();
 
-  return {
-    state,
-  };
-})();
+  // Load from localStorage on initialization
+  try {
+    const cached = localStorage.getItem(LOCAL_STORAGE_KEY);
+    if (cached) {
+      const data = JSON.parse(cached);
 
-// Initial state from localStorage.
-(async () => {
-  const localCache = localStorage.getItem(LOCAL_STORAGE_KEY);
-  if (!localCache) {
-    return;
-  }
+      // Validate and restore orderByTimeAsc
+      if (Object.hasOwn(data, "orderByTimeAsc")) {
+        state.orderByTimeAsc = Boolean(data.orderByTimeAsc);
+      }
 
-  try {
-    const cache = JSON.parse(localCache);
-    if (Object.hasOwn(cache, "orderByTimeAsc")) {
-      viewStore.state.setPartial({ orderByTimeAsc: Boolean(cache.orderByTimeAsc) });
-    }
-    if (Object.hasOwn(cache, "layout")) {
-      if (["LIST", "MASONRY"].includes(cache.layout)) {
-        viewStore.state.setPartial({ layout: cache.layout });
+      // Validate and restore layout
+      if (Object.hasOwn(data, "layout") && ["LIST", "MASONRY"].includes(data.layout)) {
+        state.layout = data.layout as LayoutMode;
       }
     }
-  } catch {
-    // Do nothing
+  } catch (error) {
+    console.warn("Failed to load view settings from localStorage:", error);
   }
+
+  /**
+   * Toggle sort order between ascending and descending
+   */
+  const toggleSortOrder = (): void => {
+    state.setPartial({ orderByTimeAsc: !state.orderByTimeAsc });
+  };
+
+  /**
+   * Set the layout mode
+   *
+   * @param layout - The layout mode to set
+   */
+  const setLayout = (layout: LayoutMode): void => {
+    state.setPartial({ layout });
+  };
+
+  /**
+   * Reset to default settings
+   */
+  const resetToDefaults = (): void => {
+    state.setPartial({
+      orderByTimeAsc: false,
+      layout: "LIST",
+    });
+  };
+
+  /**
+   * Clear persisted settings
+   */
+  const clearStorage = (): void => {
+    localStorage.removeItem(LOCAL_STORAGE_KEY);
+  };
+
+  return {
+    state,
+    toggleSortOrder,
+    setLayout,
+    resetToDefaults,
+    clearStorage,
+  };
 })();
 
 export default viewStore;
diff --git a/web/src/store/workspace.ts b/web/src/store/workspace.ts
index bde2aa0c0..e53c3241b 100644
--- a/web/src/store/workspace.ts
+++ b/web/src/store/workspace.ts
@@ -1,5 +1,11 @@
+/**
+ * Workspace Store
+ *
+ * Manages workspace-level configuration and settings.
+ * This is a server state store that fetches workspace profile and settings.
+ */
 import { uniqBy } from "lodash-es";
-import { makeAutoObservable } from "mobx";
+import { computed } from "mobx";
 import { workspaceServiceClient } from "@/grpcweb";
 import { WorkspaceProfile, WorkspaceSetting_Key } from "@/types/proto/api/v1/workspace_service";
 import {
@@ -8,74 +14,181 @@ import {
   WorkspaceSetting,
 } from "@/types/proto/api/v1/workspace_service";
 import { isValidateLocale } from "@/utils/i18n";
+import { StandardState, createServerStore } from "./base-store";
 import { workspaceSettingNamePrefix } from "./common";
+import { createRequestKey } from "./store-utils";
 
-class LocalState {
+/**
+ * Valid theme options
+ */
+const VALID_THEMES = ["default", "default-dark", "paper", "whitewall"] as const;
+export type Theme = (typeof VALID_THEMES)[number];
+
+/**
+ * Check if a string is a valid theme
+ */
+export function isValidTheme(theme: string): theme is Theme {
+  return VALID_THEMES.includes(theme as Theme);
+}
+
+/**
+ * Workspace store state
+ */
+class WorkspaceState extends StandardState {
+  /**
+   * Current locale (e.g., "en", "zh", "ja")
+   */
   locale: string = "en";
-  theme: string = "default";
+
+  /**
+   * Current theme
+   * Note: Accepts string for flexibility, but validates to Theme
+   */
+  theme: Theme | string = "default";
+
+  /**
+   * Workspace profile containing owner and metadata
+   */
   profile: WorkspaceProfile = WorkspaceProfile.fromPartial({});
+
+  /**
+   * Array of workspace settings
+   */
   settings: WorkspaceSetting[] = [];
 
-  get generalSetting() {
-    return (
-      this.settings.find((setting) => setting.name === `${workspaceSettingNamePrefix}${WorkspaceSetting_Key.GENERAL}`)?.generalSetting ||
-      WorkspaceSetting_GeneralSetting.fromPartial({})
-    );
+  /**
+   * Computed property for general settings
+   * Memoized for performance
+   */
+  get generalSetting(): WorkspaceSetting_GeneralSetting {
+    return computed(() => {
+      const setting = this.settings.find((s) => s.name === `${workspaceSettingNamePrefix}${WorkspaceSetting_Key.GENERAL}`);
+      return setting?.generalSetting || WorkspaceSetting_GeneralSetting.fromPartial({});
+    }).get();
   }
 
-  get memoRelatedSetting() {
-    return (
-      this.settings.find((setting) => setting.name === `${workspaceSettingNamePrefix}${WorkspaceSetting_Key.MEMO_RELATED}`)
-        ?.memoRelatedSetting || WorkspaceSetting_MemoRelatedSetting.fromPartial({})
-    );
+  /**
+   * Computed property for memo-related settings
+   * Memoized for performance
+   */
+  get memoRelatedSetting(): WorkspaceSetting_MemoRelatedSetting {
+    return computed(() => {
+      const setting = this.settings.find((s) => s.name === `${workspaceSettingNamePrefix}${WorkspaceSetting_Key.MEMO_RELATED}`);
+      return setting?.memoRelatedSetting || WorkspaceSetting_MemoRelatedSetting.fromPartial({});
+    }).get();
   }
 
-  constructor() {
-    makeAutoObservable(this);
-  }
+  /**
+   * Override setPartial to validate locale and theme
+   */
+  setPartial(partial: Partial): void {
+    const finalState = { ...this, ...partial };
 
-  setPartial(partial: Partial) {
-    const finalState = {
-      ...this,
-      ...partial,
-    };
-    if (!isValidateLocale(finalState.locale)) {
+    // Validate locale
+    if (partial.locale !== undefined && !isValidateLocale(finalState.locale)) {
+      console.warn(`Invalid locale "${finalState.locale}", falling back to "en"`);
       finalState.locale = "en";
     }
-    if (!["default", "default-dark", "paper", "whitewall"].includes(finalState.theme)) {
-      finalState.theme = "default";
+
+    // Validate theme - accept string and validate
+    if (partial.theme !== undefined) {
+      const themeStr = String(finalState.theme);
+      if (!isValidTheme(themeStr)) {
+        console.warn(`Invalid theme "${themeStr}", falling back to "default"`);
+        finalState.theme = "default";
+      } else {
+        finalState.theme = themeStr;
+      }
     }
+
     Object.assign(this, finalState);
   }
 }
 
+/**
+ * Workspace store instance
+ */
 const workspaceStore = (() => {
-  const state = new LocalState();
+  const base = createServerStore(new WorkspaceState(), {
+    name: "workspace",
+    enableDeduplication: true,
+  });
 
-  const fetchWorkspaceSetting = async (settingKey: WorkspaceSetting_Key) => {
-    const setting = await workspaceServiceClient.getWorkspaceSetting({ name: `${workspaceSettingNamePrefix}${settingKey}` });
-    state.setPartial({
-      settings: uniqBy([setting, ...state.settings], "name"),
-    });
-  };
+  const { state, executeRequest } = base;
 
-  const upsertWorkspaceSetting = async (setting: WorkspaceSetting) => {
-    await workspaceServiceClient.updateWorkspaceSetting({ setting });
-    state.setPartial({
-      settings: uniqBy([setting, ...state.settings], "name"),
-    });
+  /**
+   * Fetch a specific workspace setting by key
+   *
+   * @param settingKey - The setting key to fetch
+   */
+  const fetchWorkspaceSetting = async (settingKey: WorkspaceSetting_Key): Promise => {
+    const requestKey = createRequestKey("fetchWorkspaceSetting", { key: settingKey });
+
+    return executeRequest(
+      requestKey,
+      async () => {
+        const setting = await workspaceServiceClient.getWorkspaceSetting({
+          name: `${workspaceSettingNamePrefix}${settingKey}`,
+        });
+
+        // Merge into settings array, avoiding duplicates
+        state.setPartial({
+          settings: uniqBy([setting, ...state.settings], "name"),
+        });
+      },
+      "FETCH_WORKSPACE_SETTING_FAILED",
+    );
   };
 
-  const getWorkspaceSettingByKey = (settingKey: WorkspaceSetting_Key) => {
-    return (
-      state.settings.find((setting) => setting.name === `${workspaceSettingNamePrefix}${settingKey}`) || WorkspaceSetting.fromPartial({})
+  /**
+   * Update or create a workspace setting
+   *
+   * @param setting - The setting to upsert
+   */
+  const upsertWorkspaceSetting = async (setting: WorkspaceSetting): Promise => {
+    return executeRequest(
+      "", // No deduplication for updates
+      async () => {
+        await workspaceServiceClient.updateWorkspaceSetting({ setting });
+
+        // Update local state
+        state.setPartial({
+          settings: uniqBy([setting, ...state.settings], "name"),
+        });
+      },
+      "UPDATE_WORKSPACE_SETTING_FAILED",
     );
   };
 
-  const setTheme = async (theme: string) => {
+  /**
+   * Get a workspace setting from cache by key
+   * Does not trigger a fetch
+   *
+   * @param settingKey - The setting key
+   * @returns The cached setting or an empty setting
+   */
+  const getWorkspaceSettingByKey = (settingKey: WorkspaceSetting_Key): WorkspaceSetting => {
+    const setting = state.settings.find((s) => s.name === `${workspaceSettingNamePrefix}${settingKey}`);
+    return setting || WorkspaceSetting.fromPartial({});
+  };
+
+  /**
+   * Set the workspace theme
+   * Updates both local state and persists to server
+   *
+   * @param theme - The theme to set
+   */
+  const setTheme = async (theme: string): Promise => {
+    // Validate theme
+    if (!isValidTheme(theme)) {
+      console.warn(`Invalid theme "${theme}", ignoring`);
+      return;
+    }
+
+    // Update local state immediately
     state.setPartial({ theme });
 
-    // Update the workspace setting - store theme in a custom field or handle differently
+    // Persist to server
     const generalSetting = state.generalSetting;
     const updatedGeneralSetting = WorkspaceSetting_GeneralSetting.fromPartial({
       ...generalSetting,
@@ -92,28 +205,65 @@ const workspaceStore = (() => {
     );
   };
 
+  /**
+   * Fetch workspace profile
+   */
+  const fetchWorkspaceProfile = async (): Promise => {
+    const requestKey = createRequestKey("fetchWorkspaceProfile");
+
+    return executeRequest(
+      requestKey,
+      async () => {
+        const profile = await workspaceServiceClient.getWorkspaceProfile({});
+        state.setPartial({ profile });
+        return profile;
+      },
+      "FETCH_WORKSPACE_PROFILE_FAILED",
+    );
+  };
+
   return {
     state,
     fetchWorkspaceSetting,
+    fetchWorkspaceProfile,
     upsertWorkspaceSetting,
     getWorkspaceSettingByKey,
     setTheme,
   };
 })();
 
-export const initialWorkspaceStore = async () => {
-  const workspaceProfile = await workspaceServiceClient.getWorkspaceProfile({});
-  // Prepare workspace settings.
-  for (const key of [WorkspaceSetting_Key.GENERAL, WorkspaceSetting_Key.MEMO_RELATED]) {
-    await workspaceStore.fetchWorkspaceSetting(key);
-  }
+/**
+ * Initialize the workspace store
+ * Called once at app startup to load workspace profile and settings
+ *
+ * @throws Never - errors are logged but not thrown
+ */
+export const initialWorkspaceStore = async (): Promise => {
+  try {
+    // Fetch workspace profile
+    const workspaceProfile = await workspaceStore.fetchWorkspaceProfile();
 
-  const workspaceGeneralSetting = workspaceStore.state.generalSetting;
-  workspaceStore.state.setPartial({
-    locale: workspaceGeneralSetting.customProfile?.locale,
-    theme: "default",
-    profile: workspaceProfile,
-  });
+    // Fetch required settings
+    await Promise.all([
+      workspaceStore.fetchWorkspaceSetting(WorkspaceSetting_Key.GENERAL),
+      workspaceStore.fetchWorkspaceSetting(WorkspaceSetting_Key.MEMO_RELATED),
+    ]);
+
+    // Apply settings to state
+    const workspaceGeneralSetting = workspaceStore.state.generalSetting;
+    workspaceStore.state.setPartial({
+      locale: workspaceGeneralSetting.customProfile?.locale || "en",
+      theme: "default",
+      profile: workspaceProfile,
+    });
+  } catch (error) {
+    console.error("Failed to initialize workspace store:", error);
+    // Set default fallback values
+    workspaceStore.state.setPartial({
+      locale: "en",
+      theme: "default",
+    });
+  }
 };
 
 export default workspaceStore;