# How Plane Differentiates Between Server State and Client State: SWR vs MobX Architecture

> Discover how Plane's architecture separates server state with SWR from client state with MobX. Learn about their distinct roles in managing remote data and local UI.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: architecture
- Published: 2026-06-22

---

**Plane uses SWR to manage server state (remote API data with caching and revalidation) and MobX to handle client state (local UI observables and transient data), maintaining strict architectural boundaries between the two.**

In the `makeplane/plane` codebase, state management is split into two distinct layers based on data origin and lifecycle requirements. Understanding how Plane differentiates between server state and client state is essential for contributing to the frontend architecture or building custom extensions. This separation leverages SWR for HTTP‑centric remote data and MobX for reactive local application state.

## Architectural Separation: Server State vs Client State

Plane draws a clear line between where data lives and how it flows:

| Aspect | Server State (SWR) | Client State (MobX) |
|--------|-------------------|---------------------|
| **Library** | SWR – React hooks for data fetching, caching, and revalidation | MobX – Observable state containers with automatic dependency tracking |
| **Data Source** | Backend APIs (e.g., workspace labels, cycles, estimates) | UI‑centric values (e.g., selected workspace, loading flags, panel visibility) |
| **Location** | Hook files under `apps/web/core/hooks/` | Store files under `packages/shared-state/src/store/` |
| **Lifecycle** | Fetched on demand, cached globally by SWR, revalidated on focus or interval | Instantiated at app startup, persists for the session, updates propagate automatically |
| **Mutability** | Immutable cache entries; refreshed via API calls | Mutable observables updated through actions |

## Server State Management with SWR

Plane uses SWR for any data that originates from the backend and requires caching, stale‑while‑revalidate strategies, or automatic background updates.

### Implementation in Hooks

SWR logic is encapsulated in custom hooks under `apps/web/core/hooks/`. For example, `useWorkspaceIssueProperties` fetches workspace metadata using SWR’s `useSWR` hook with specific fetch keys:

```tsx
// apps/web/core/hooks/use-workspace-issue-properties.ts
import useSWR from "swr";
import { WORKSPACE_LABELS, WORKSPACE_CYCLES } from "@/constants/fetch-keys";

export const useWorkspaceIssueProperties = (workspaceSlug) => {
  // fetch workspace labels
  useSWR(
    workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
    workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null,
    { revalidateIfStale: false, revalidateOnFocus: false }
  );

  // fetch workspace cycles
  useSWR(
    workspaceSlug ? WORKSPACE_CYCLES(workspaceSlug.toString()) : null,
    workspaceSlug ? () => fetchWorkspaceCycles(workspaceSlug.toString()) : null,
    { revalidateIfStale: false, revalidateOnFocus: false }
  );
};

```

The hook returns SWR’s standard API (`data`, `error`, `mutate`, `isValidating`), keeping components agnostic to the underlying HTTP implementation.

### Lifecycle and Caching

SWR stores data in a global cache keyed by unique strings (e.g., `WORKSPACE_LABELS(workspaceSlug)`). This guarantees that multiple components subscribing to the same key receive identical data without duplicate network requests. According to the Plane source code, fetch keys are centralized in [`packages/constants/src/swr.ts`](https://github.com/makeplane/plane/blob/main/packages/constants/src/swr.ts) to ensure consistency across the application.

## Client State Management with MobX

For transient UI state that never requires server round‑trips, Plane implements MobX stores in `packages/shared-state/src/store/`.

### Store Structure and Observables

MobX stores are plain classes decorated with `makeObservable`, converting properties into observable refs and methods into actions:

```ts
// packages/shared-state/src/store/workspace.store.ts
import { makeObservable, observable, action, runInAction } from "mobx";

export class WorkspaceStore {
  id = "";
  name = "";
  createdAt = "";
  updatedAt = "";
  // …other props

  constructor() {
    makeObservable(this, {
      id: observable.ref,
      name: observable.ref,
      createdAt: observable.ref,
      updatedAt: observable.ref,
      setWorkspace: action,
    });
  }

  setWorkspace(data: Partial<WorkspaceStore>) {
    runInAction(() => {
      Object.assign(this, data);
    });
  }
}

```

Components consume these stores through the `observer` HOC from `mobx-react-lite`, ensuring automatic re‑renders when observed properties change.

### Reactive Updates

Because MobX tracks dependencies automatically, UI toggles like `workspaceStore.isPanelOpen = !workspaceStore.isPanelOpen` trigger immediate updates without explicit subscription boilerplate. This pattern is ideal for optimistic UI updates and local layout state.

## Integration Pattern: How SWR Feeds MobX

Plane does not treat these layers as silos; instead, it follows a **fetch‑then‑hydrate** pattern:

1. **Fetching** – SWR retrieves raw JSON from the server via HTTP.
2. **Hydration** – A MobX store action (e.g., `setWorkspace`) receives the fetched payload and writes it into observable fields.
3. **UI Consumption** – `observer` components render the MobX data, while SWR handles background revalidation and cache updates.

This architecture ensures that the UI always reacts to the local MobX state (fast, synchronous updates) while SWR maintains the server‑side source of truth in the background.

## Key Files and Locations

| Area | File Path |
|------|-----------|
| **SWR Hooks** | [`apps/web/core/hooks/use-workspace-issue-properties.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/hooks/use-workspace-issue-properties.ts) |
| **SWR Constants** | [`packages/constants/src/swr.ts`](https://github.com/makeplane/plane/blob/main/packages/constants/src/swr.ts) |
| **MobX Stores** | [`packages/shared-state/src/store/workspace.store.ts`](https://github.com/makeplane/plane/blob/main/packages/shared-state/src/store/workspace.store.ts) |
| **User Store** | [`packages/shared-state/src/store/user.store.ts`](https://github.com/makeplane/plane/blob/main/packages/shared-state/src/store/user.store.ts) |
| **Filter Store** | [`packages/shared-state/src/store/work-item-filters/filter.store.ts`](https://github.com/makeplane/plane/blob/main/packages/shared-state/src/store/work-item-filters/filter.store.ts) |

## Summary

- **SWR manages server state** in `apps/web/core/hooks/`, handling remote data fetching, caching, and revalidation via unique fetch keys.
- **MobX manages client state** in `packages/shared-state/src/store/`, providing reactive observables for UI‑centric data through actions and `makeObservable`.
- **Integration** follows a fetch‑then‑hydrate pattern: SWR fetches API data, then MobX stores hydrate observables that components consume via `observer`.
- **Clear boundaries** prevent mixing concerns—SWR never stores local UI flags, and MobX never initiates HTTP requests directly.

## Frequently Asked Questions

### Why does Plane use both SWR and MobX instead of one library?

SWR excels at caching, deduplication, and background revalidation of remote data, while MobX provides efficient, fine‑grained reactivity for local state mutations. Combining them allows Plane to benefit from SWR’s HTTP‑specific optimizations (stale‑while‑revalidate, focus revalidation) alongside MobX’s synchronous, observable updates for UI interactions.

### How does Plane handle optimistic updates with this dual approach?

Optimistic updates mutate the MobX store immediately (e.g., `taskStore.updateTaskLocally(task)`), giving the user instant feedback. Meanwhile, the SWR `mutate` function reconciles the change with the server in the background. If the server rejects the update, SWR’s cache can be rolled back and the MobX store updated to reflect the server state.

### Where are SWR fetch keys defined in the Plane codebase?

Fetch keys are centralized in [`packages/constants/src/swr.ts`](https://github.com/makeplane/plane/blob/main/packages/constants/src/swr.ts) (e.g., `WORKSPACE_LABELS`, `WORKSPACE_CYCLES`). This ensures consistent cache keys across multiple hooks and prevents cache fragmentation when different components request the same resource.

### Can MobX stores trigger SWR revalidation directly?

No, Plane maintains strict separation: MobX stores do not import SWR or trigger HTTP requests. Instead, components or service layers call SWR’s `mutate` function after successful API mutations, which updates the global cache and optionally triggers background revalidation. MobX remains focused on local state representation only.