# How Securo Implements Multi-Workspace Support: A Deep Dive into the React Architecture

> Discover how Securo implements multi-workspace support using React context. Learn about centralized state, localStorage persistence, and scoped API requests and data.

- Repository: [securo-finance/securo](https://github.com/securo-finance/securo)
- Tags: deep-dive
- Published: 2026-08-28

---

**Securo's multi-workspace support is built around a dedicated React context (`WorkspaceContext`) that centralizes workspace state, persists the active workspace ID in `localStorage`, and automatically scopes all API requests and cached data to the selected workspace.**

This architecture enables users to seamlessly switch between isolated workspaces—such as different companies or teams—while the server remains stateless and simply filters data based on a `workspace_id` header. In this article, we'll examine exactly how `securo-finance/securo` implements this pattern, from the core context provider to the API integration layer.

---

## The Core Architecture: WorkspaceContext

At the heart of Securo's multi-workspace system lies `WorkspaceContext`, defined in [`frontend/src/contexts/workspace-context.tsx`](https://github.com/securo-finance/securo/blob/main/frontend/src/contexts/workspace-context.tsx). This React context serves as the single source of truth for all workspace-related state and actions throughout the application.

The `WorkspaceProvider` wraps the entire application hierarchy (see [`frontend/src/App.tsx`](https://github.com/securo-finance/securo/blob/main/frontend/src/App.tsx)), ensuring every component can access workspace state through the `useWorkspace` hook. This provider manages three critical responsibilities:

- **Workspace list management** – Fetches and stores all workspaces the authenticated user can access
- **Active workspace resolution** – Determines which workspace is currently active based on persisted preferences
- **Permission derivation** – Computes user capabilities (`canManage`, `canWrite`) based on their role in the active workspace

---

## Persisting Workspace Selection Across Sessions

Securo stores the active workspace ID in the browser's `localStorage` under the key `workspace_id`, defined as `WORKSPACE_STORAGE_KEY` in [`frontend/src/lib/api.ts`](https://github.com/securo-finance/securo/blob/main/frontend/src/lib/api.ts). This persistence layer serves two purposes: it remembers the user's selection across page reloads, and it provides the value that the API interceptor reads to attach the workspace header.

When the `WorkspaceProvider` mounts, it executes `loadWorkspaces` to reconcile the stored ID with the server-side workspace list:

```tsx
const loadWorkspaces = useCallback(async () => {
  setIsLoading(true);
  try {
    const fetched = await workspacesApi.list(); // GET /api/workspaces
    setList(fetched);
    // Reconcile stored workspace ID with the fetched list
    const storedId = localStorage.getItem(WORKSPACE_STORAGE_KEY);
    const found = fetched.find(w => w.id === storedId);
    if (found) setCurrentId(found.id);
    else if (fetched.length) {
      const fallbackId = fetched[0].id;
      localStorage.setItem(WORKSPACE_STORAGE_KEY, fallbackId);
      setCurrentId(fallbackId);
    } else {
      localStorage.removeItem(WORKSPACE_STORAGE_KEY);
      setCurrentId(null);
    }
  } finally {
    setIsLoading(false);
  }
}, []);

```

This reconciliation handles edge cases gracefully: if the stored workspace ID is stale (user was removed from a workspace) or missing entirely, the provider falls back to the first available workspace.

---

## Switching Workspaces and Cache Invalidation

The `switchWorkspace` function demonstrates Securo's careful approach to data isolation. When a user selects a different workspace, the system must ensure no cached data from the previous workspace bleeds through:

```tsx
// inside WorkspaceProvider
const switchWorkspace = useCallback(async (id: string) => {
  if (id === currentId) return;
  // Persist the new ID so the interceptor includes it on the next request
  localStorage.setItem(WORKSPACE_STORAGE_KEY, id);
  setCurrentId(id);
  // Invalidate all queries that were scoped to the previous workspace
  await queryClient.resetQueries();
}, [currentId, queryClient]);

```

The `queryClient.resetQueries()` call is critical here—it immediately clears React Query's entire cache, forcing all active queries to refetch with the new `workspace_id` header. This prevents transactions, rules, or payees from Workspace A from briefly appearing while Workspace B loads.

---

## API Integration: Automatic Header Injection

Securo's API layer in [`frontend/src/lib/api.ts`](https://github.com/securo-finance/securo/blob/main/frontend/src/lib/api.ts) implements workspace scoping transparently through an Axios request interceptor. Every outgoing HTTP request automatically receives the `workspace_id` header from `localStorage`, without individual API calls needing to manage this concern:

```typescript
// Conceptual representation of the interceptor pattern in api.ts
api.interceptors.request.use((config) => {
  const workspaceId = localStorage.getItem(WORKSPACE_STORAGE_KEY);
  if (workspaceId) {
    config.headers['workspace_id'] = workspaceId;
  }
  return config;
});

```

This design keeps server-side logic simple: the backend remains stateless and merely filters all data by the `workspace_id` header present in each request. No session-based workspace tracking or complex authentication contexts are required on the server.

---

## Consuming Workspace Context in UI Components

Components throughout Securo consume `useWorkspace` to access the active workspace and permission checks. The `workspace-switcher` component in [`frontend/src/components/workspace-switcher.tsx`](https://github.com/securo-finance/securo/blob/main/frontend/src/components/workspace-switcher.tsx) renders the workspace selection UI and invokes `switchWorkspace` on user selection.

Page components like **Transactions**, **Rules**, **Payees**, and **Budgets** use the context to scope their data fetching:

```tsx
const { current, canWrite } = useWorkspace();
const { data: transactions } = useQuery(
  ['transactions', current?.id],
  () => transactionsApi.list(current!.id),
  { enabled: !!current }
);

```

Including `current?.id` in the `queryKey` ensures React Query maintains separate cache entries per workspace. The `canWrite` boolean gates UI actions—buttons for creating or editing records only appear when the user has write permissions in the active workspace.

---

## Permission Model and Access Control

The `WorkspaceContext` derives permission booleans from the user's role in the active workspace:

- **`current`** – The full workspace object (id, name, settings)
- **`role`** – The user's role string (e.g., 'admin', 'member', 'viewer')
- **`canManage`** – Whether the user can administer workspace settings and members
- **`canWrite`** – Whether the user can create and modify data within the workspace

These computed properties centralize authorization logic, preventing scattered permission checks and ensuring consistent UI behavior. When `current` changes via workspace switching, all derived permissions recalculate automatically.

---

## Key Implementation Files

| File | Responsibility |
|------|---------------|
| [`frontend/src/contexts/workspace-context.tsx`](https://github.com/securo-finance/securo/blob/main/frontend/src/contexts/workspace-context.tsx) | Core React context with provider, state management, and `useWorkspace` hook |
| [`frontend/src/lib/api.ts`](https://github.com/securo-finance/securo/blob/main/frontend/src/lib/api.ts) | `WORKSPACE_STORAGE_KEY` constant and Axios interceptor for header injection |
| [`frontend/src/components/workspace-switcher.tsx`](https://github.com/securo-finance/securo/blob/main/frontend/src/components/workspace-switcher.tsx) | UI component for workspace selection |
| [`frontend/src/App.tsx`](https://github.com/securo-finance/securo/blob/main/frontend/src/App.tsx) | Application root that wraps the tree with `WorkspaceProvider` |
| Page components ([`transactions.tsx`](https://github.com/securo-finance/securo/blob/main/transactions.tsx), [`rules.tsx`](https://github.com/securo-finance/securo/blob/main/rules.tsx), etc.) | Consumers that scope queries and permissions to the active workspace |

---

## Summary

Securo's multi-workspace implementation demonstrates a clean separation of concerns between client-side state management and server-side data filtering:

- **WorkspaceContext** centralizes all workspace state and actions in a single React context
- **localStorage persistence** remembers user preferences across sessions with graceful fallback handling
- **Query cache invalidation** ensures complete data isolation when switching workspaces
- **Axios interceptor** transparently injects the workspace header without burdening individual API calls
- **Permission derivation** provides consistent access control throughout the UI

This architecture lets Securo deliver a seamless multi-workspace experience with minimal server complexity—the backend simply trusts the `workspace_id` header and returns appropriately filtered data.

---

## Frequently Asked Questions

### How does Securo remember which workspace I was using after I refresh the page?

Securo stores the active workspace ID in `localStorage` under the key `workspace_id`. When the application loads, the `WorkspaceProvider` reads this value and validates it against the list of workspaces you have access to. If the stored ID is valid, that workspace becomes active; otherwise, it falls back to your first available workspace.

### What happens to my data when I switch workspaces?

All cached data is immediately cleared via `queryClient.resetQueries()` when `switchWorkspace` is called. This ensures you never see stale data from a previous workspace. React Query then refetches fresh data using the new `workspace_id` header, so each workspace maintains completely isolated data views.

### Why does Securo use a header instead of a URL path for workspace scoping?

The `workspace_id` header approach keeps URLs clean and consistent across workspaces. It also simplifies bookmarking and sharing—links to `/transactions` work regardless of which workspace is active, since the workspace context travels with the user's session rather than the URL. The server remains stateless and simply filters by the header value.

### How do permission checks work across different workspaces?

The `WorkspaceContext` derives `canManage` and `canWrite` booleans from your role in the currently active workspace. When you switch workspaces, these values recalculate automatically based on your role in the new workspace. This means you might have full admin rights in one workspace but read-only access in another, with the UI adapting instantly to show only permitted actions.