Skip to content

State Management

The Cloud Frontend splits state into two clear categories:

CategoryToolExamples
Server state (remote data)TanStack Query v5Incidents list, asset details, notifications badge
Client/UI stateZustandAuth tokens, dashboard layout, WebSocket connection

All stores live in Frontend/lib/stores/.

The primary auth store. Persisted to localStorage (user metadata only — never the access token).

KeyTypeNotes
userUser | nullCurrent user object from GET /api/v1/me
access_tokenstring | nullJWT access token — in memory only, not persisted
permissionsstring[]Resolved permission set for the current user

Methods: setUser(), setAccessToken(), clearAuth(), hasPermission(permission: string)

Because access_token is not persisted, every full page reload triggers SessionRestore to call POST /api/v1/auth/refresh before rendering protected pages.

Dashboard layout state: widget positions, visible widgets, drag-and-drop grid configuration. Persisted to localStorage and synced to the server via PATCH /api/v1/dashboard/layout.

WebSocket connection state for the security and VPN hubs.

KeyTypeNotes
securityWsWebSocket | nullActive /api/ws/security connection
vpnWsWebSocket | nullActive /api/ws/vpn connection
securityStatus'connecting' | 'open' | 'closed'UI connection indicator
vpnStatus'connecting' | 'open' | 'closed'UI connection indicator

WS connections are opened when the relevant module page mounts and closed on unmount.

The QueryClient is configured in Frontend/src/main.tsx with:

  • staleTime: varies per query (short for notifications, longer for static data like permission catalog)
  • refetchInterval: 60 seconds for notifications badge
  • Global retry: 1 (prevents hammering the API on 5xx)
  • Error handling: 401 responses trigger authStore.clearAuth() and redirect to /login

Each domain has a dedicated Axios client in Frontend/lib/api/<domain>.ts:

// Example pattern (illustrative)
export const incidentsApi = {
list: (params) => apiClient.get('/incidents', { params }),
get: (id) => apiClient.get(`/incidents/${id}`),
create: (data) => apiClient.post('/incidents', data),
update: (id, data) => apiClient.patch(`/incidents/${id}`, data),
};

The shared apiClient (Axios instance in lib/config/api.ts) automatically attaches Authorization: Bearer <access_token> from authStore on every request.

The Axios response interceptor catches 401 responses, attempts POST /api/v1/auth/refresh, and retries the original request with the new token. If the refresh also fails (expired cookie), authStore.clearAuth() is called and the user is redirected to /login.

When the security or VPN WebSocket receives a relevant event, it invalidates the corresponding TanStack Query cache key:

WS event { type: "alert" }
→ queryClient.invalidateQueries(['security', 'alerts'])
→ TanStack Query refetches alert list
→ UI updates automatically

This avoids manual state management for real-time data — WebSocket acts as a cache invalidation signal, and TanStack Query handles the actual data fetching.