Skip to content

Data Flow & State

This page traces a request from a button click to a backend microservice and back, and explains how the platform divides responsibility between server state (React Query) and client state (Zustand).

The Request Path

Every backend interaction flows through the same layers. The browser speaks REST/JSON to a Next.js route handler; the route handler speaks gRPC to the backend.

sequenceDiagram
    participant C as Component
    participant H as apiHook (React Query)
    participant S as service/*
    participant AX as Axios (apis/index.ts)
    participant R as app/api/* route
    participant CB as Cerbos
    participant G as utils/grpc.ts
    participant MS as Backend microservice

    C->>H: call hook (useQuery / useMutation)
    H->>S: invoke service fn
    S->>AX: axios.get/post(...)
    AX->>AX: attach JWT from cookies
    AX->>R: REST/JSON request
    R->>CB: authorize(principal, resource, action)
    CB-->>R: allow / deny
    R->>G: typed gRPC client call
    G->>MS: Protocol Buffers over gRPC
    MS-->>G: proto response
    G-->>R: { success, data, status }
    R-->>AX: JSON
    AX-->>H: data (or 401 → refresh flow)
    H-->>C: data / isLoading / error

The type → service → hook → component pattern

A single feature is wired through four small, predictable files:

proto/projects.proto              # contract (compiled to proto-types/projects.ts)
types/createProject.types.ts      # TS request/response types (re-exported from proto)
services/createProject.services.ts# axios call to app/api/projects
hooks/apiHooks/useCreateProject   # useMutation wrapper + cache invalidation
components/pages/projects/...      # form that calls the hook

A representative end-to-end trace — "Create Project":

  1. A form (React Hook Form + Ant Design) submits to useCreateProject().
  2. The hook runs a React Query useMutation whose mutationFn calls the createProject service.
  3. The service issues axios.post('projects', payload); the Axios interceptor attaches the JWT.
  4. The request hits app/api/projects/route.ts, which authorizes via Cerbos, then calls the ProjectsService gRPC client from utils/grpc.ts.
  5. The backend returns a typed CreateProjectResponse; the route handler returns JSON.
  6. On success the hook invalidates ['projects'] so the list re-fetches, and navigates to /projects/[id].

Backend Transports

The platform uses three transports depending on the interaction:

flowchart LR
    subgraph Client
        B[Browser]
    end
    subgraph NextServer["Next.js server"]
        API[app/api/*]
    end
    subgraph BE[Backend]
        G[gRPC microservices]
        WS[Socket.io server]
    end

    B -->|"REST / JSON (Axios)"| API
    API -->|"gRPC + protobuf (nice-grpc)"| G
    B <-->|"WebSocket (Socket.io)"| WS
Transport Used for Where configured
REST/JSON (Axios) All standard browser→server requests apis/index.ts
gRPC (nice-grpc) Server→backend microservice calls utils/grpc.ts, utils/microservices.ts
WebSocket (Socket.io) Realtime crawler-log streaming, activity feeds hooks/useLogStream.hook.tsx, store/crawlerLogsStore.ts

The gRPC client factory resolves a cluster address of the form {service}.{env}.svc.grepsr.net:443, applies TLS based on CONFIG_SECURE_CONNECTION, and raises the max message size to 30 MB (large report payloads exceed the 4 MB default). gRPC status codes are mapped to HTTP status codes (utils/mapGrpcToHttpStatus) so the browser sees conventional HTTP semantics.

State Management

The platform draws a hard line between server state and client state.

flowchart TB
    subgraph RQ["React Query — SERVER state"]
        Q["Queries: projects, reports, activities, ..."]
        M["Mutations: createProject, bulkExport, deleteRule, ..."]
    end
    subgraph ZU["Zustand — CLIENT state (store/)"]
        AppS["app: projects cache, selectedReport, filters, schedule params"]
        AuthS["auth: userData, tokens, impersonateUser"]
        PageS["pageRef: element refs"]
        LogS["crawlerLogs: live log stream"]
    end
    Backend[(Backend)] --> RQ
    RQ -. hydrates .-> ZU
    UI[Components] --> RQ
    UI --> ZU

React Query (server state)

  • Configured in providers/QueryProvider.tsx. Anything originating from the backend — lists, details, activity feeds — is a query; anything that writes is a mutation.
  • Defaults favour explicit control: a short staleTime, refetchOnWindowFocus disabled, and a single retry. Freshness is driven by targeted invalidateQueries after mutations rather than aggressive background refetching.
  • Optimistic updates (utils/optimisticUpdate) apply changes to the cache immediately and roll back on error for latency-sensitive interactions.

Zustand (client state)

  • Configured in store/index.ts, combining slices with the Redux DevTools middleware. Accessed via the useStore() hook.
Slice Holds
app Cached project list, the currently selected report, filtered report subsets, schedule-form parameters.
auth userData, JWT tokens, and the impersonateUser flag for admin impersonation.
pageRef DOM/element references shared across the page.
crawlerLogs The live log stream populated by the Socket.io hook.

Rule of thumb

If the data came from (or is going to) the backend, it belongs in React Query. If it is ephemeral UI or session context that the client owns, it belongs in Zustand. Avoid copying server data into Zustand except as a deliberate cache (as app.projects does for cross-route convenience).

Error Handling

Errors are handled at consistent layers:

  1. Service layer — services surface a typed error from the Axios call.
  2. Axios interceptor (apis/index.ts) — a 401 triggers the token-refresh-and-retry flow (see Authentication & Authorization); other errors propagate.
  3. React QueryisError/error are exposed to components; mutation onError handlers run rollbacks where applicable.
  4. UI — failures render as toasts/notifications via the Toaster providers.